Skip to content

Starting your KosmoJS journey is a breeze! ✨

Begin your project with a solid foundation. KosmoJS provides a structured yet flexible starting point designed for real-world applications with multiple concerns.

In just a few commands, you'll have a fully-configured Vite project with:

  • Directory-based routing - Folders become URLs automatically
  • Type-safe APIs - Runtime validation from TypeScript types
  • Auto-generated fetch clients - Typed client-side validation before requests
  • OpenAPI documentation - API specs generated automatically
  • Route-level middleware - Hierarchical organization without imports
  • Client pages - React, SolidJS, or Vue with the same routing patterns
  • Nested routes with layouts - Share UI structure across route groups
  • Server-side rendering - With critical CSS inlining and static files serving
  • Multiple source folders - Separate apps in a single monorepo-like project

πŸš€ Create Your Project ​

sh
pnpm dlx kosmojs my-app
sh
npx kosmojs my-app
sh
yarn dlx kosmojs my-app

Navigate to your project:

sh
cd my-app

All subsequent commands run from here.

πŸ“¦ Install Dependencies ​

This is absolutely necesarry to continue!

sh
pnpm install
sh
npm install
sh
yarn install

πŸ“ Create Your First Source Folder ​

Unlike standard Vite templates, KosmoJS doesn't create a source folder automatically.

Instead, you create source folders as needed - one for your main app, another for an admin panel, a third for a marketing site, and so on.

Each source folder is completely independent with its own configuration, base URL, and dev server port.

sh
pnpm +folder
sh
npm run +folder
sh
yarn +folder

You'll configure:

  • Folder name - e.g., front
  • Base URL - Where this app serves from (default: /)
  • Dev server port - Port for development (default: 4000)
  • Frontend framework - SolidJS, React, Vue, or none for API-only folders
  • SSR - Enable server-side rendering (more on this later)

Non-interactive mode:

For CI/CD or scripting, skip the prompts by providing options directly:

sh
pnpm +folder --name front --base / --port 4000 --framework solid --ssr
sh
npm run +folder -- --name front --base / --port 4000 --framework solid --ssr
sh
yarn +folder --name front --base / --port 4000 --framework solid --ssr

Available options:

  • --name <name> - Source folder name (required in non-interactive mode)
  • --base <path> - Base URL (default: /)
  • --port <number> - Development server port (default: 4000)
  • --framework <framework> - Frontend framework: none, solid, react, vue
  • --ssr - Enable server-side rendering

The source folder adds dependencies. Install them:

sh
pnpm install
sh
npm install
sh
yarn install

πŸ›£οΈ About Directory-Based Routing ​

Before we create routes, let's understand how KosmoJS maps folders to URLs.

The core principle:

  • Folder names become URL path segments
  • Each route requires an index file.
txt
api/
  index/
    index.ts          ➜ /api
  users/
    index.ts          ➜ /api/users
    [id]/
      index.ts        ➜ /api/users/:id

pages/
  index/
    index.tsx         ➜ /
  users/
    index.tsx         ➜ /users
    [id]/
      index.tsx       ➜ /users/:id

Key benefits:

  • No routing config files - The file system IS the routing table
  • API and pages mirror each other - Easy to see how frontend and backend relate
  • Colocalization - Each route folder can contain helpers, types, tests
  • Dynamic parameters - Use [id] for required params, [[id]] for optional, [...path] for rest params

This works identically for both API routes and client pages, so you learn the pattern once.

Learn more about routing patterns β†’

πŸ’‘ Essential Path Mappings ​

Your project starts with a minimal tsconfig.json:

tsconfig.json
json
{
  "extends": "@kosmojs/config/tsconfig.vite.json"
}

The extended tsconfig.vite.json contains essential path mappings that enable your application to work properly.

You can add additional paths, but these prefixes are reserved and must not be overridden:

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

Overriding these prefixes will break your application.

Each prefix maps to a specific part of your project:

~/* - Root-level imports

Access files at your project root:

ts
import globalMiddleware from "~/core/api/use";

@/* - Source folder imports

Import from your src/ directory without the src/ prefix. The src/ directory contains your source folders (src/admin/, src/front/, src/app/, etc.):

ts
import LoginForm from "@/front/components/LoginForm";
import config from "@/admin/config";

_/* - Generated code imports

Access generated files from lib/src/, which mirrors your src/ directory structure:

ts
import fetchMap from "_/front/fetch";            // All fetch clients
import { GET } from "_/front/fetch/users/[id]";  // Specific fetch client

All generated files - api routes, validators, fetch clients - live in lib/src/ and are accessible via the _/ prefix.

βš™οΈ Create Your First API Route ​

In your source folder, create api/users/[id]/index.ts file:

txt
src/front/
└── api/
    └── users/
        └── [id]/
            └── index.ts

KosmoJS detects the new file and generates boilerplate automatically.

Some editors show it immediately; others need you to briefly refocus the file.

You'll see this structure appear:

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

export default defineRoute(({ GET }) => [
  GET(async (ctx) => {
    ctx.body = "Automatically generated route: [ users/[id] ]"
  }),
]);

Let's make it actually useful. Replace the generated code with:

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

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

export default defineRoute(({ GET }) => [
  GET(async (ctx) => {
    const { id } = ctx.params; 

    // In a real app, this would query your database
    const user: User = {
      id: Number(id),
      name: "Jane Smith",
      email: "jane@example.com",
    };

    ctx.body = user;
  }),
]);

Start the dev server:

sh
pnpm dev
sh
npm run dev
sh
yarn dev

Visit http://localhost:4000/api/users/123 and you'll see your response.

The route works, but there's no validation yet. Let's fix that.

More details: API Routes β†’

πŸ›‘οΈ Add Runtype Validation ​

Here's where it gets interesting - KosmoJS automatically converts your TypeScript types into high-performance runtime validators.

It uses the state-of-the-art TypeBox library to convert your types into JSON Schema validators - no magic, just solid tooling.

Parameter Validation ​

Route parameters come from URL paths. For example, the URL /api/users/123 is matched by the api/users/[id] route, where [id] captures 123. Since URLs are text, parameters arrive as strings.

KosmoJS validates and converts them in two steps:

  • Validates against your schema
  • Casts numbers automatically - number types get converted via Number()

Access validated parameters via ctx.typedParams instead of ctx.params.

Here's an example validating id as a number:

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

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

export default defineRoute<[
  number // validate id as number
]>(({ GET }) => [
  GET(async (ctx) => {
    const { id } = ctx.typedParams; // id is a validated number!

    const user: User = {
      id, // No conversion needed - already a number
      name: "Jane Smith",
      email: "jane@example.com",
    };

    ctx.body = user;
  }),
]);

The <[number]> type argument to defineRoute validates parameters as a tuple. Each position corresponds to a route parameter in order.

The validated params are available through ctx.typedParams. While ctx.params still exists, it is not typed and all params are strings.

You can refine parameters further by using TRefine (globally available, no need to import):

ts
defineRoute<[
  TRefine<number, { minimum: 1, multipleOf: 1 }>
]>(...)

This ensures the id is not only a number, but a positive integer.

If someone requests /api/users/abc, validation fails before your handler runs.

Payload Validation ​

Request payloads need validation too. Here's the key concept:

  • On GET requests, ctx.payload mirrors ctx.query (query parameters).
  • On POST/PUT/PATCH requests, ctx.payload contains the request body.

Let's add a POST endpoint that creates users:

api/users/index.ts
ts
import { defineRoute } from "_/front/api/users";

type CreateUserPayload = {
  name: string;
  email: TRefine<string, { format: "email" }>;
  age?: number;
}

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

export default defineRoute(({ POST }) => [
  POST<
    CreateUserPayload // payload schema
  >(async (ctx) => {
    // ctx.payload is the validated request body
    const { name, email, age } = ctx.payload;

    const newUser: User = {
      id: 1,
      name,
      email,
      age,
    };

    ctx.body = newUser;
  }),
]);

The first type argument to POST validates the payload. For POST/PUT/PATCH, this validates the request body. For GET, this would validate query parameters.

If the payload doesn't match CreateUserPayload, validation fails with a detailed error before your handler runs.

Response Validation ​

You can also validate what your handlers return. This catches bugs where you accidentally return incomplete or malformed data:

api/users/index.ts
ts
export default defineRoute(({ POST }) => [
  POST<
    CreateUserPayload, // payload schema
    User // response schema
  >(async (ctx) => {
    const { name, email, age } = ctx.payload;

    const newUser: User = {
      id: 1,
      name,
      email,
      age,
    };

    ctx.body = newUser; // Response validated before sending!
  }),
]);

The second type argument validates ctx.body before sending the response. If your handler returns data that doesn't match User, validation throws an error instead of sending invalid data to clients.

For our GET endpoint:

api/users/[id]/index.ts
ts
export default defineRoute<[number]>(({ GET }) => [
  GET<
    never, // skip payload validation
    User // response schema
  >(async (ctx) => {
    const { id } = ctx.typedParams;

    const user: User = {
      id,
      name: "Jane Smith",
      email: "jane@example.com",
    };

    ctx.body = user; // Validated!
  }),
]);

We use never for the first argument because here GET request don't have a payload we want to validate (though you could validate query parameters if needed).

More details: Runtype Validation β†’

⇆ Generated Fetch Clients ​

Here's the continuation of powerful validation pattern: KosmoJS generates fully-typed fetch clients that validate on the client side before sending requests.

Fetch clients use the exact same high-performance validation schemas as your server. No shifts, no drifts - client-side validation produces identical results to server-side validation.

Import the generated fetch client for type-safe API calls with built-in validation.

Method 1: Direct import (recommended for most cases)

tsx
import { useParams, createAsync } from "@solidjs/router";
import { GET } from "_/front/fetch/users/[id]"; 

export default function UserPage() {
  const params = useParams();
  const user = createAsync(() => GET([params.id])); 
  // ...
}
tsx
import { useState, useEffect } from "react";
import { useParams } from "react-router";
import { GET } from "_/front/fetch/users/[id]"; 

export default function UserPage() {
  const params = useParams();
  const [user, setUser] = useState(null);

  useEffect(() => {
    GET([params.id]).then(setUser); 
  }, [params.id]);

  // ...
}
vue
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { useRoute } from "vue-router";
import { GET } from "_/front/fetch/users/[id]"; 

const route = useRoute();
const user = ref(null);

onMounted(async () => {
  user.value = await GET([route.params.id]); 
});
</script>

<template>
  <div>
    <h1>{{ user?.name }}</h1>
    <p>{{ user?.email }}</p>
  </div>
</template>

Method 2: Via fetch map (useful for dynamic routing)

tsx
import fetchMap from "_/front/fetch";

// Access routes through the centralized map
const userFetch = fetchMap["users/[id]"];
const response = await userFetch.GET([123]); // Tuple matches route params

The fetch map exports all your API routes in one place, which is handy when you need to:

  • Dynamically select which endpoint to call
  • Iterate over multiple routes
  • Build tools that work with your entire API surface

The fetch client:

  • Is fully typed - autocomplete shows exact parameters/payload structure
  • Validates data client-side before making requests
  • Automatically infers response types

Invalid requests never reach your server, saving bandwidth and giving instant feedback.

More details: Fetch Clients β†’

πŸ“‹ Automatic OpenAPI Documentation ​

OpenAPI (formerly Swagger) is the industry standard for documenting REST APIs. It provides machine-readable API specifications that power interactive documentation, client code generators, and testing tools.

KosmoJS can automatically generate complete OpenAPI 3.1 specifications for your API.

Enable OpenAPI generator in your vite.config.ts:

src/front/vite.config.ts
ts
import devPlugin from "@kosmojs/dev";
import {
  apiGenerator,
  fetchGenerator,
  typeboxGenerator,
  openapiGenerator, 
} from "@kosmojs/generators";

export default {
  plugins: [
    devPlugin(apiurl, {
      generators: [
        apiGenerator(),
        fetchGenerator(),
        typeboxGenerator(),
        openapiGenerator({ 
          outfile: "openapi.json",
          info: {
            title: "My API",
            version: "1.0.0",
          },
        }),
      ],
    }),
  ],
}

The dev server should restart automatically, but after adding new generators it's recommended to manually stop and restart it with pnpm dev

That's it. The generator analyzes your routes, type definitions, and validation schemas to produce a complete OpenAPI spec.

What gets generated:

  • All route paths with HTTP methods
  • Parameter schemas (from your tuple types)
  • Request body schemas (from payload types)
  • Response schemas (from response types)

You can use the generated openapi.json with tools like Swagger UI, Postman, or any OpenAPI-compatible client generator.

More details: OpenAPI Generator β†’

πŸ” Add Middleware ​

KosmoJS uses Koa for its API layer, following Koa's middleware model.

Middleware are functions that execute before your route handlers, handling cross-cutting concerns like authentication, logging, error handling, etc.

Each middleware receives the request context (ctx) and a next function. Call next() to pass control to the next middleware or handler in the chain. Skip next() to stop the chain early (useful for rejecting unauthorized requests).

The straightforward approach is to import and wire middleware manually:

api/users/[id]/index.ts
ts
import { logRequest } from "@/middleware/logging";
import { defineRoute } from "_/front/api/users/[id]";

export default defineRoute(({ GET, use }) => [
  use(logRequest), // Wire it manually

  GET(async (ctx) => {
    // Handler logic
  }),
]);

This works perfectly for simple APIs with a couple of endpoints.

But as your API grows, this approach becomes tedious. Every new route needs the same imports. Every middleware change means updating multiple files. Authentication across 20 routes? That's 20 files to maintain.

Here's a better way for larger APIs: Route-level middleware files.

Create api/users/use.ts:

txt
src/front/
└── api/
    └── users/
        β”œβ”€β”€ use.ts          πŸ’€ Wraps all routes under /users
        └── [id]/
            └── index.ts

KosmoJS generates boilerplate:

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

export default use([
  async (ctx, next) => {
    // Your middleware logic here
    return next();
  },
]);

Add authentication:

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

export default use([
  async (ctx, next) => {
    const token = ctx.headers.authorization?.replace("Bearer ", ""); 
    ctx.assert(token, 401, "Authentication required");

    const user = await verifyToken(token);
    ctx.assert(user, 401, "Invalid token");

    ctx.state.user = user; // Available in all child routes
    return next();
  },
]);

Every route under /api/users now requires authentication. No imports, no repetition.

The use.ts file creates a middleware hierarchy that mirrors your route structure. Parent folders wrap child routes automatically.

More details: Middleware β†’

🎨 Create Client Pages ​

Now let's create the frontend. Pages live in the pages/ folder and follow the same directory-based routing as API routes.

The pages/ folder is where your client-side UI lives. Each index.tsx (React/SolidJS) or index.vue (Vue) file contains a page component written in whichever framework you selected during source folder creation.

KosmoJS enforces strict architectural boundaries: client code never runs on your API server, and API code never runs in the browser.

Even in SSR mode, your client code runs on a separate server (different process, different port, potentially different machine) - your API server stays lean and focused.

Create users page - pages/users/index.tsx (or .vue):

txt
src/front/
└── pages/
    └── users/
        └── index.tsx

KosmoJS detects the new file and generates framework-specific boilerplate.

Some editors show it immediately; others need you to briefly refocus the file.

Key points:

  • Pages use index.tsx (React/SolidJS) or index.vue (Vue)
  • Parameters work identically to API routes
  • Auto-generated boilerplate adapts to your chosen framework

Add Layouts for Shared UI ​

As your application grows, you'll need to share UI elements across multiple pages - navigation bars, sidebars, headers, footers. Without layouts, you'd duplicate these in every page component.

Layouts solve this by wrapping groups of related pages with common UI structure. A layout renders once and your page components render inside it.

For example, all pages under /users might share the same navigation sidebar. Instead of copying that sidebar code into every user-related page, you create one layout that wraps them all.

Now that you have pages, let's organize them with layouts.

Create pages/users/layout.tsx:

txt
src/front/
└── pages/
    └── users/
        β”œβ”€β”€ layout.tsx      πŸ’€ Wraps all pages under /users
        └── index.tsx

All routes under /users now render inside this layout. The navigation bar appears on every page automatically.

Layouts can be nested - deeper layouts wrap inner layouts, creating a hierarchy that matches your route structure.

More details: Nested Routes & Layouts β†’

⚑ Server-Side Rendering ​

Want SEO, faster initial page loads, and critical CSS optimization? Enable SSR.

Note: You can enable SSR when creating a source folder with the --ssr flag (or by selecting it in the interactive mode).

If you didn't enable it initially, enable it in your vite.config.ts:

src/front/vite.config.ts
ts
import devPlugin from "@kosmojs/dev";
import {
  apiGenerator,
  fetchGenerator,
  typeboxGenerator,
  ssrGenerator, 
} from "@kosmojs/generators";

export default {
  plugins: [
    devPlugin(apiurl, {
      generators: [
        apiGenerator(),
        fetchGenerator(),
        typeboxGenerator(),
        ssrGenerator(), 
      ],
    }),
  ],
}

The dev server should restart automatically, but after adding new generators it's recommended to manually stop and restart it with pnpm dev

KosmoJS creates entry/server.ts - your SSR orchestration file.

This is where you control the server-side rendering process. By default, it uses renderToString which renders the entire page before sending it.

For more advanced scenarios, you can switch to renderToStream to enable streaming SSR, where the browser receives and displays content progressively as it's rendered.

ts
export default renderFactory(() => {
  const hydrationScript = generateHydrationScript();
  return {
    async renderToString(url, { criticalCss }) {
      const router = await createRouter(App, routes, { url });
      const head = criticalCss.reduce(
        (head, { text }) => `${head}\n<style>${text}</style>`,
        hydrationScript,
      );
      const html = renderToString(() => router);
      return { head, html };
    },
  };
});
ts
export default renderFactory(() => {
  return {
    async renderToString(url, { criticalCss }) {
      const router = await createRouter(App, routes, { url });
      const head = criticalCss
        .map(({ text }) => `<style>${text}</style>`)
        .join("\n");
      const html = renderToString(router);
      return { head, html };
    },
  };
});
ts
export default renderFactory(() => {
  return {
    async renderToString(url, { criticalCss }) {
      const app = createSSRApp(App);
      await createRouter(app, routes, { url });
      const head = criticalCss
        .map(({ text }) => `<style>${text}</style>`)
        .join("\n");
      const html = await renderToString(app);
      return { head, html };
    },
  };
});

The critical CSS optimization is automatic:

  • Analyzes which CSS your route actually uses
  • Extracts only the necessary styles
  • Inlines them in the <head> for instant rendering
  • Loads remaining styles asynchronously

Your pages render instantly with styled content - no flash of unstyled content, no render-blocking CSS.

Build and run: ​

sh
pnpm build
node dist/front/ssr/server.js -p 4001

Your app is now server-rendered with optimized CSS delivery.

Worth Noting: During build, KosmoJS bundles two separate servers:

  • API server for handling API requests
  • SSR server for rendering your UI

You can deploy them separately, to different machines, scale them independently, or run them side-by-side.

Your API server stays clean and dedicated to handling API requests, while the SSR server is dedicated entirely to rendering your app.

More details on SSR: SolidJS Β· React Β· Vue

🌐 Scale with Multiple Source Folders ​

As your app grows, add more source folders for different concerns:

Add admin source folder - Vue framework, no SSR:

sh
pnpm +folder
# name: admin
# baseurl: /admin
# port: 4001
# framework: vue
sh
pnpm +folder --name admin --base /admin --port 4002 --framework vue

Add marketing source folder - SolidJS framework, with SSR:

sh
pnpm +folder
# name: marketing
# baseurl: /
# port: 4002
# framework: solid
# SSR: enabled
sh
pnpm +folder --name marketing --base / --port 4002 --framework solid --ssr

Each source folder is completely independent:

  • Different framework if needed (SolidJS for marketing, Vue for admin)
  • Different base URL (routes auto-prefix correctly)
  • Different dev server port (run all simultaneously)
  • Independent vite.config.ts (customize per folder)
  • Separate api/ and pages/ directories

Run everything with a single command:

sh
pnpm dev

All source folders run in parallel. One command, entire application running.

πŸ—οΈ What You've Built ​

In less than 30 minutes, you have:

βœ… Type-safe APIs with compile-time and runtime validation

βœ… Generated fetch clients that validate before sending requests

βœ… Directory-based routing that maps folders to URLs naturally

βœ… Hierarchical middleware that wraps routes without imports

βœ… Nested layouts that compose automatically

βœ… Server-side rendering with critical CSS optimization

βœ… Multi-app architecture with independent source folders

And here's the key: you defined your types once. KosmoJS generated:

  • Runtime validators for parameters, payloads, and responses
  • Typed fetch clients with client-side validation
  • OpenAPI schemas for documentation
  • Type definitions for perfect autocomplete

Your development workflow stays clean and fast:

  • Hot Module Replacement for instant updates
  • No build step during development
  • TypeScript errors in your editor immediately
  • Generated code lives in lib/, not cluttering your source

➑ Next Steps ​

Learn the patterns:

Explore advanced features:


KosmoJS provides structure, not constraints. Your project, your rules.

Start building with better organization from day one.

Released under the MIT License.