Skip to content

A step-by-step walkthrough covering everything KosmoJS provides.

Create a Project ​

sh
npm create kosmo demo

A short interactive setup creates the project together with your first source folder, prompting for the framework and backend.

The folder itself defaults to app at base / (override later, or via flags in CLI mode).

It is also possible to bootstrap in the current folder, just use . as name:

sh
npm create kosmo .
Prefer a scripted setup?

Provide the framework/backend up front and no prompts appear:

sh
npm create kosmo demo -- --frontend solid --backend hono

The first source folder is always app, serving pages at / and its API at /api. Both frontend and backend can be further configured in kosmo.config.ts.

Need no backend? Provide --no-backend flag:

sh
npm create kosmo demo -- --frontend solid --no-backend

Same for the frontend, provide the --no-frontend flag to get a backend-only setup.


After bootstrap, cd into freshly created project (unless the project was bootstrapped in the current folder):

sh
cd ./demo

Install Dependencies ​

sh
npm install

The scaffold is complete, not a skeleton - everything comes filled in. What it doesn't have yet is routes, and that is what you add next.

Start the dev server ​

The dev server watches your routes and recomputes as you work - and seeds starter code into any route or page file you create empty:

sh
npm run dev

Your app is now running at http://localhost:4556.

Create Your First API Route ​

Create api/users/[id]/index.ts - KosmoJS detects the file and seeds boilerplate:

ts
// Hono: api/users/[id]/index.ts
import { defineRoute } from "_/api";

export default defineRoute<"users/[id]">(({ GET }) => [
  GET(async (ctx) => {
    return ctx.text("users/[id] route starts here - replace this response with real logic.");
  }),
]);

Some editors show seeded content immediately; others need a brief unfocus/refocus.

Replace with real logic:

ts
// Hono: api/users/[id]/index.ts
import { defineRoute } from "_/api";

export default defineRoute<"users/[id]">(({ GET }) => [
  GET(async (ctx) => {
    const { id } = ctx.req.param();
    const user = { id, name: "Jane Smith", email: "jane@example.com" };
    return ctx.json(user);
  }),
]);

With dev server running, visit http://localhost:4556/api/users/123:

Details ›

Add Validation ​

Parameter Validation ​

Pass a tuple as the second type argument to refine params. Each position maps to a route parameter in order. Validation works identically across all frameworks, read validated params via *.validated.params:

ts
type User = { id: number; name: string; email: string }

export default defineRoute<"users/[id]", [
  number 
]>(({ GET }) => [
  GET(async (ctx) => {
    const { id } = ctx.validated.params; // id is a validated number
    // ...
  }),
]);

Use VRefine for additional constraints (no import needed):

ts
defineRoute<"users/[id]", [
  VRefine<number, { minimum: 1, multipleOf: 1 }> // positive integer
]>

Raw params still can be accessed via ctx.req.param()/event.context.params/ctx.params, but they are untyped strings - prefer ctx.validated.params.

Payload/Response Validation ​

The first type argument to each method handler defines validation targets.

Metadata targets (any method): query · headers · cookies

Body targets (mutually exclusive, POST/PUT/PATCH only): json · form · raw

ts
// Hono: api/users/index.ts
import type { CreateUserPayload, User } from "./types";

export default defineRoute<"users">(({ POST }) => [
  POST<{
    json: CreateUserPayload,
    response: [200, "json", User] 
  }>(async (ctx) => {
    const { name, email, age } = ctx.validated.json;
    return ctx.json({ id: 1, name, email, age });
  }),
]);
./types.ts
ts
export type CreateUserPayload = {
  name: string;
  email: VRefine<string, { format: "email" }>;
  age?: number;
}

export type User = { id: number; name: string; email: string }

Payload is validated before your handler runs. Response is validated before it's sent.

Details ›

Add Middleware ​

For simple cases, wire middleware inline with use:

ts
import { logRequest } from "~/middleware/logging";

export default defineRoute<"users/[id]">(({ use, GET }) => [
  use(logRequest),
  GET(async (ctx) => { /* ... */ }),
]);

For anything shared across routes, use cascading middleware instead. Create api/users/use.ts - it wraps every route under /api/users automatically:

api/users/use.ts
ts
import { use } from "_/api";

export default [
  use(async (ctx, next) => {
    // runs for every route under /api/users
    return next();
  })
];

No imports in route files, no repetition. Parent use.ts files wrap child routes automatically.

Details ›

Fetch Clients ​

Fetch clients are fully typed and validated client-side using the same high-performance TypeBox validators as the server - identical results, no duplication, no drift.

Invalid requests are caught before they leave the browser:

tsx
// React: pages/users/[id]/index.tsx
import { useState, useEffect } from "react";
import { useParams } from "react-router";
import fetchClients from "_/fetch";

const { GET } = fetchClients["users/[id]"];

export default function UserPage() {
  const params = useParams();
  const [user, setUser] = useState(null);
  useEffect(() => { GET([params.id]).then(setUser); }, [params.id]);
  // ...
}

Server-side validation still runs even when endpoints are called directly - client validation is additive, not a substitute.

Details ›

Create Client Pages ​

Pages live in pages/ and follow the same directory-based routing as API routes. Create pages/users/index.tsx - KosmoJS seeds framework-specific boilerplate.

Add a layout for shared UI across route groups - create pages/users/layout.tsx:

txt
pages/
└── users/
    ├── layout.tsx    ← wraps all pages under /users
    └── index.tsx

Layouts can be nested - deeper layouts wrap inner layouts, matching your route hierarchy.

Details ›

Server-Side Rendering ​

Every folder has it on already. To turn it off, or back on, edit kosmo.config.ts:

kosmo.config.ts
ts
import { defineConfig } from "@kosmojs/dev";

export default defineConfig({
  frontend: {
    // ...
    ssr: true,
  },
});

Restart dev server after changing kosmo.config.ts.

KosmoJS seeds entry/server.ts - your SSR orchestration file. Critical CSS is extracted and inlined automatically; remaining styles load asynchronously.

See it running - pnpm preview builds and serves the production output, and rebuilds whenever you save:

sh
pnpm preview

For deployment, pnpm build writes dist/run.js, which serves every source folder from one process. Folders are also bundled separately, so they can be deployed, scaled and run independently when that suits you better.

Details › · Dev / Build / Run ›

Add More Source Folders ​

Your project starts with the source folder created at bootstrap. As the app grows, add more - one per distinct concern (main app, admin panel, marketing site, etc.).

Each is independent with its own set of frameworks, config, base URL, etc.

sh
npm run folder

You'll be prompted for the frontend, backend, and more. The name gives the folder its prefixes: pages at /<name>, API at /<name>/api.

Non-interactive mode is also supported; pass any flag and no prompts appear:

  • --frontend solid|react|vue|svelte|mdx or --no-frontend (one required)
  • --backend hono|h3|koa or --no-backend (one required)
  • --overwrite to proceed into a non-empty directory

Everything else - SSR, SSG, TanStack Query - is a key in the folder's kosmo.config.ts, not a flag at creation.

sh
npm run folder -- <name> --frontend solid --backend hono

Need no backend? Provide the --no-backend flag for a frontend-only folder (a static docs or marketing site).

Need no client? Provide the --no-frontend flag for a backend-only folder (an API service with no UI).

The choice is always explicit - a forgotten flag is an error, never a silent default:

sh
npm run folder -- api  --backend hono --no-frontend   # API only, no UI
npm run folder -- docs --frontend mdx --no-backend    # UI only, no backend

Creating a source folder adds framework-specific dependencies. Install them:

sh
npm install

Directory-Based Routing ​

Folder names become URL segments. Each route requires an index file:

txt
api/
  users/
    index.ts          -> /api/users
    [id]/
      index.ts        -> /api/users/:id

pages/
  users/
    index.tsx         -> /users
    [id]/
      index.tsx       -> /users/:id

Parameters: [id] required · {id} optional · {...path} splat. Same pattern for API and pages - learn once, use everywhere.

Details ›

Path Mappings ​

Your project starts with a minimal tsconfig.json:

tsconfig.json
json
{
  "extends": "./lib/tsconfig.json",
  "include": ["./lib/*.d.ts"]
}

The extended config provides path mappings used throughout the framework. You can add your own paths, but these prefixes are reserved:

  • @/* - Root-level imports
  • ~/* - Source folder imports
  • _/* - Derived code imports

Project Structure › walks the whole layout - what lives in src/ versus lib/, and what each alias resolves to.


Next Steps ​

Core patterns: Routing · Validation · Middleware · Layouts · Fetch Clients

Advanced: VRefine · OpenAPI · Production Builds

Released under the MIT License.