---
url: /about.md
description: >-
KosmoJS - the composable meta-framework. Organize multiple apps with
directory-based routing, automatic runtime validation, typed fetch clients,
and OpenAPI generation.
---
`KosmoJS` is named after the Greek "Kosmos" (κόσμος) - "order" or "world" -
reflecting the focus on organized, structured project architecture.
### 💡 What it does and how
Most projects eventually need more than one app - a marketing site, a customer dashboard,
an admin panel. The usual options work well but at the price of inevitable friction:
**Microservices** - each app in its own repo with its own `package.json`,
its own CI pipeline, its own deploy config. Shared types drift.
A database schema change means coordinating across repos.
You spend more time on infrastructure than on features.
**Monorepos** - everything in one repo, but now you manage
workspaces, package boundaries, internal dependency graphs, build caching configs,
and a `packages/shared` folder that becomes a dumping ground.
The tooling that's supposed to simplify things becomes its own project.
**DIY glue** - skip the tooling, wire it yourself. Shared scripts, custom build steps,
a hand-rolled dev server that stitches apps together. It works at first.
Then the project grows, a second developer joins, and nobody remembers
why `start-all.sh` passes `--legacy-peer-deps` or which app breaks
if you update the shared config. Homegrown infrastructure is cheap to build
and expensive to maintain.
::: info There's a simpler way
**KosmoJS** takes a different, **Vite**-inspired approach: **Source Folders**.
:::
Each app lives in its own folder with its own framework stack, base URL, and build output -
but they're not separate packages. They share one `package.json`, one `node_modules`,
one database layer, one set of types. You choose backend/frontend framework for each source folder,
while routing and validation patterns statys the same across all frameworks:
```
src/
├── app/ - React, Hono backend, base "/app"
├── admin/ - Vue, Koa backend, base "/admin"
└── marketing/ - MDX, no backend, base "/"
```
Need a type from the customer app in the admin dashboard? Import it.
Changed a database model? Every source folder sees the change immediately.
No publishing, no versioning, no workspace protocols.
Each folder has independent routing, middleware, layouts, config, and deploy configuration.
One command starts them all. One command builds them all.
And you can build or deploy a single folder when that's all you need.
### 🛠️ Under the hood
Add a source folder, pick a backend (`Koa` or `Hono`) and a frontend (`React`, `Vue`, `SolidJS`, or `MDX`).
Create files in `api/` and `pages/`, and they become routes automatically:
```
src/app/
├── api/
│ └── users/
│ └── [id]/
│ └── index.ts ➜ GET /api/users/:id
└── pages/
└── users/
└── [id]/
└── index.tsx ➜ /users/:id
```
`KosmoJS` acts as a universal chassis - providing the same consistent way to define routes
for all source folders, regardless of framework, backend or frontend.
Thanks to this architecture, `KosmoJS` provides:
* [End-to-End Type Safety](/validation/intro)
* [Generated Fetch Clients + OpenAPI](/fetch/intro)
* [Composable Cascading Middleware](/backend/middleware)
* [Nested Layouts](/frontend/layouts)
and [More Features ➜](/features)
### ⚖️ How it differs
Most meta-frameworks choose your frontend framework for you and own your deployment model.
Monorepo tools give you flexibility but bury you in configuration.
Microservices give you independence but fragment your codebase.
DIY glue works until it doesn't - and by then it's load-bearing.
`KosmoJS` takes the best of each: the structure of a monorepo,
the simplicity of a single project, and the independence of separate apps -
without the overhead of any of them.
You keep full control over backend, frontend, state management, styling, database, and deploy target.
`KosmoJS` handles routing conventions, validation pipeline, middleware composition, development workflow and build orchestration.
You focus on features, `KosmoJS` takes care of infrastructure.
***
---
---
url: /backend/intro.md
description: >-
KosmoJS API layer supports both Koa and Hono frameworks with elegant
middleware composition, end-to-end type safety, and flexible route definitions
inspired by Sinatra framework.
---
`KosmoJS`'s API layer supports two frameworks: [Koa](https://koajs.com/) and [Hono](https://hono.dev/).
**Koa** - battle-tested, mature ecosystem, elegant async/await middleware, Node.js-focused.
**Hono** - exceptional performance, runs on Node.js, Deno, Bun, Cloudflare Workers, and other edge platforms unchanged.
Route organization, middleware patterns, and validation are identical between the two.
The difference is the context API inside handlers - each framework has its own.
## 🔧 Defining Endpoints
Every API route exports a `defineRoute` definition as its default export.
The factory function receives HTTP method builders and `use` for middleware,
and returns an array of handlers. Destructure only what you need:
```ts [api/users/[id]/index.ts]
import { defineRoute } from "_/api";
export default defineRoute<"users/[id]">(({ GET }) => [
GET(async (ctx) => {
// handle GET /users/:id
}),
]);
```
Multiple methods in one route:
```ts [api/users/index.ts]
export default defineRoute(({ GET, POST, PUT, DELETE }) => [
GET(async (ctx) => { /* retrieve */ }),
POST(async (ctx) => { /* create */ }),
PUT(async (ctx) => { /* update */ }),
DELETE(async (ctx) => { /* delete */ }),
]);
```
Handler order doesn't matter - requests are dispatched by HTTP method.
Undefined methods return `405 Method Not Allowed` automatically.
Available builders: `HEAD`, `OPTIONS`, `GET`, `POST`, `PUT`, `PATCH`, `DELETE`.
This method-based routing style draws inspiration from [Sinatra](https://sinatrarb.com/) -
the Ruby framework that pioneered it back in 2007.
## 🛡️ Type Safety
Parameters, payloads, and responses are all typed through `TypeScript` type arguments -
the same definitions drive both compile-time checking and runtime validation.
No separate schema language, no DSL switching.
([Details ➜ ](/backend/type-safety))
## ▶️ Middleware
The `use` function gives you fine-grained middleware control at the route level,
complementing global and cascading middleware.
([Details ➜ ](/backend/middleware))
---
---
url: /frontend/application.md
description: >-
Generator-produced foundation files for React, SolidJS, Vue and MDX
applications - root App component, router configuration, and client entry
point with SSR hydration support.
---
Each framework generator produces a small set of foundation files that wire up
routing, navigation, and application bootstrap. The structure is consistent
across frameworks: a root App component, a router configuration, and a client
entry point.
## 🎨 Root Application Component
The generator creates a minimal root component as your application shell.
Extend it with global layouts, error boundaries, authentication providers, or
other application-wide concerns.
::: code-group
```tsx [React · App.tsx]
import { Outlet } from "react-router";
export default function App() {
return ;
}
```
```tsx [SolidJS · App.tsx]
import type { ParentComponent } from "solid-js";
const App: ParentComponent = (props) => {
return props.children;
};
export default App;
```
```vue [Vue · App.vue]
```
```mdx [MDX · App.mdx]
{props.children}
```
:::
## 🛣️ Router Configuration
The `routerFactory` function connects your root App component and generated
routes to the framework's native router.
It accepts a callback receiving auto-generated route definitions from `KosmoJS`.
The callback must return two functions:
* `clientRouter()` - browser-based routing for client-side navigation
* `serverRouter(url)` - server-side routing for SSR, receiving the requested URL
::: code-group
```tsx [React · router.tsx]
import {
createBrowserRouter,
createStaticHandler,
createStaticRouter,
RouterProvider,
StaticRouterProvider,
} from "react-router";
import { baseurl } from "~/config";
import routerFactory from "_/router";
import App from "./App";
export default routerFactory((routes) => {
const routeStack = [
{
path: "/",
Component: App,
children: routes,
},
];
const handler = createStaticHandler(routeStack, { basename: baseurl });
return {
async clientRouter() {
const router = createBrowserRouter(routeStack, { basename: baseurl });
return ;
},
async serverRouter(url) {
const context = await handler.query(new Request(url.href));
if (context instanceof Response) {
// handled by SSR server
throw context;
}
const router = createStaticRouter(routeStack, context);
return ;
},
};
});
```
```tsx [SolidJS · router.tsx]
import { Router } from "@solidjs/router";
import { baseurl } from "~/config";
import routerFactory from "_/router";
import App from "./App";
export default routerFactory((routes) => {
return {
async clientRouter() {
return {routes};
},
async serverRouter(url) {
return
{routes}
;
},
}
});
```
```ts [Vue · router.ts]
import { createApp, createSSRApp } from "vue";
import {
createMemoryHistory,
createRouter,
createWebHistory,
} from "vue-router";
import { baseurl } from "~/config";
import routerFactory from "_/router";
import App from "./App.vue";
export default routerFactory((routes) => {
return {
async clientRouter() {
const app = createApp(App);
const router = createRouter({
history: createWebHistory(baseurl),
routes,
strict: true,
});
app.use(router);
return app;
},
async serverRouter(url) {
const app = createSSRApp(App);
const router = createRouter({
history: createMemoryHistory(baseurl),
routes,
strict: true,
});
await router.push(url.pathname.replace(baseurl, ""));
await router.isReady();
app.use(router);
return app;
},
};
});
```
```tsx [MDX · router.tsx]
import { createRouter } from "_/mdx";
import routerFactory from "_/router";
import App from "./App.mdx";
import { components } from "./components/mdx"
export default routerFactory((routes) => {
const router = createRouter(routes, App, { components });
return {
async clientRouter() {
return router.resolve();
},
async serverRouter(url) {
return router.resolve(url);
},
};
});
```
:::
All use your source folder's `baseurl` config for correct path-based
routing. The generated `routes` are always wrapped inside your `App` component,
establishing the layout hierarchy.
## 🎯 Application Entry
The `entry/client.tsx` file is your application's DOM rendering entry point,
referenced from `index.html`:
```html
```
Vite begins from this HTML file, follows the import to `entry/client`, and
constructs the complete application dependency graph from there.
The `renderFactory` function orchestrates two rendering modes via a callback
that must return:
* `mount()` - mounts the application fresh in the browser
* `hydrate()` - hydrates pre-rendered server HTML for interactivity
On page load, `renderFactory` reads Vite's `import.meta.env.SSR` flag to select the
correct method: `hydrate()` for SSR hydration, `mount()` for a fresh client-only mount.
::: code-group
```tsx [React · entry/client.tsx]
import { createRoot, hydrateRoot } from "react-dom/client";
import renderFactory, { createRoutes } from "_/entry/client";
import routerFactory from "../router";
const routes = createRoutes({ withPreload: true });
const { clientRouter } = routerFactory(routes);
const root = document.getElementById("app");
if (root) {
renderFactory(() => {
return {
async mount() {
const page = await clientRouter();
createRoot(root).render(page);
},
async hydrate() {
const page = await clientRouter();
hydrateRoot(root, page);
},
};
});
} else {
console.error("❌ Root element not found!");
}
```
```tsx [SolidJS · entry/client.tsx]
import { hydrate, render } from "solid-js/web";
import renderFactory, { createRoutes } from "_/entry/client";
import routerFactory from "../router";
const routes = createRoutes({ withPreload: true });
const { clientRouter } = routerFactory(routes);
const root = document.getElementById("app");
if (root) {
renderFactory(() => {
return {
async mount() {
const page = await clientRouter();
render(() => page, root);
},
async hydrate() {
const page = await clientRouter();
hydrate(() => page, root)
},
}
});
} else {
console.error("❌ Root element not found!");
}
```
```ts [Vue · entry/client.ts]
import renderFactory, { createRoutes } from "_/entry/client";
import routerFactory from "../router";
const routes = createRoutes();
const { clientRouter } = routerFactory(routes);
const root = document.getElementById("app");
if (root) {
renderFactory(() => {
return {
async mount() {
const page = await clientRouter();
page.mount(root);
},
async hydrate() {
const page = await clientRouter();
page.mount(root, true);
},
};
});
} else {
console.error("❌ Root element not found!");
}
```
```tsx [MDX · entry/client.tsx]
import { hydrate, render } from "preact";
import renderFactory, { createRoutes } from "_/entry/client";
import routerFactory from "../router";
const routes = createRoutes();
const { clientRouter } = routerFactory(routes);
const root = document.getElementById("app");
if (root) {
renderFactory(() => {
return {
async mount() {
const page = await clientRouter();
render(page.component, root);
},
async hydrate() {
const page = await clientRouter();
hydrate(page.component, root);
},
};
});
} else {
console.error("❌ Root element not found!");
}
```
:::
* React uses `createRoot`/`hydrateRoot` from `react-dom/client`.
* SolidJS uses `render`/`hydrate` from `solid-js/web`.
* Vue constructs separate app instances via `createApp` and `createSSRApp`.
* MDX uses `render`/`hydrate` from `preact`.
---
---
url: /routing/generated-content.md
description: >-
KosmoJS automatically generates boilerplate code for new routes with
context-aware templates for API endpoints using defineRoute and
framework-specific page components.
---
When you create a new route file, `KosmoJS` detects it and generates appropriate boilerplate immediately.
The output differs based on whether the file is an API route or a client page, and which framework you're using.
> Some editors load generated content instantly, others may require you to briefly unfocus
> and refocus the file to see the new content.
## ⚙️ API Routes
Creating `api/users/[id]/index.ts` generates:
::: code-group
```ts [Koa]
import { defineRoute } from "_/api";
export default defineRoute<"users/[id]">(({ GET }) => [
GET(async (ctx) => {
ctx.body = "Automatically generated route: [ users/[id] ]";
}),
]);
```
```ts [Hono]
import { defineRoute } from "_/api";
export default defineRoute<"users/[id]">(({ GET }) => [
GET(async (ctx) => {
ctx.text("Automatically generated route: [ users/[id] ]");
}),
]);
```
:::
The `_/` import prefix maps to `lib/` - generated code that provides full type definitions
for all your routes. `_/api` resolves to `lib/front/api.ts`, where `front` is your source folder name.
## 🎨 Client Pages
Creating `pages/users/[id]/index.tsx` generates a minimal framework component:
```tsx [pages/users/[id]/index.tsx]
export default function Page() {
return
Automatically generated Page: [ users/[id] ]
;
}
```
The placeholder text includes your framework name and route path.
The component is named `Page` by default - rename it to something meaningful as you build it out.
> Avoid anonymous arrow functions for default exports - they can break Vite's HMR.
---
---
url: /backend/building-for-production.md
description: >-
Build and deploy KosmoJS applications to production with independent source
folder builds, deployment strategies for containers, serverless, and edge
runtimes.
---
Each source folder builds independently.
```sh
pnpm build # all source folders
pnpm build front # specific folder
```
## 📦 Build Output
```txt
dist/
└── front
├── api
│ ├── app.js # app factory (Koa) / app instance (Hono)
│ └── server.js # bundled API server
├── client
│ ├── assets/ # scripts, styles, images
│ └── index.html
└── ssr
├── app.js # SSR app factory (Vite)
└── server.js # SSR server bundle
```
The SSR output is only present when [SSR is enabled](/frontend/server-side-render).
## 🚀 Running in Production
The simplest deployment - just run the bundled server directly:
```sh
node dist/front/api/server.js
```
For more control, use the app factory at `dist/*/api/app.js`.
**Koa** - `app.callback()` is a Node.js `(IncomingMessage, ServerResponse)` handler.
Deno and Bun support it via their `node:http` compat layer, not via their native serve APIs:
```js [Node / Deno / Bun]
import { createServer } from "node:http";
import app from "./dist/front/api/app.js";
createServer(app.callback()).listen(3000);
```
**Hono** - `app.fetch` is a Web Fetch API handler, so it plugs into each runtime's native server directly:
::: code-group
```js [Node]
import { createServer } from "node:http";
import { getRequestListener } from "@hono/node-server";
import app from "./dist/front/api/app.js";
createServer(getRequestListener(app.fetch)).listen(3000);
```
```ts [Deno]
import app from "./dist/front/api/app.js";
Deno.serve({ port: 3000 }, app.fetch);
```
```ts [Bun]
import app from "./dist/front/api/app.js";
Bun.serve({ port: 3000, fetch: app.fetch });
```
:::
---
---
url: /backend/cascading-middleware.md
description: >-
Organize middleware hierarchically using use.ts files that wrap route
subtrees. Apply authentication, logging, and custom parsers to folders and
their descendants without cluttering individual route definitions.
---
Place a `use.ts` file in any folder, and its middleware automatically wraps all routes
in that folder and its subfolders - no imports or wiring required.
## 🎯 How it Works
```txt
api/users/
├── about/
│ └── index.ts
├── account/
│ ├── index.ts
│ └── use.ts
├── index.ts
└── use.ts
```
* `users/use.ts` wraps all routes under `/api/users`
* `users/account/use.ts` wraps only routes under `/api/users/account`
Execution order for a request to `/api/users/account`:
```txt
api/use.ts → global middleware
users/use.ts → parent folder
users/account/use.ts → current folder
users/account/index.ts → route handler
```
Parent middleware always runs before child middleware. Child routes cannot skip parent `use.ts`.
The generated boilerplate when you create a new `use.ts`:
```ts [api/users/use.ts]
import { use } from "_/api";
export type ExtendT = {};
export default [
use(async (ctx, next) => {
return next();
})
];
```
> Some editors load the generated content immediately, others require a brief unfocus/refocus.
Beside the default exported middleware, every `use.ts` exports an `ExtendT` type - even if empty.
This type extends the context for all routes underneath, giving you
automatic type safety for anything the middleware adds.
## 🔗 Type-Safe Context Extension
The whole point of cascading middleware is to avoid manual wiring.
That applies to types too - if your auth middleware adds `user` to the context,
every route underneath should know about it without importing or declaring anything.
`ExtendT` makes this work. Define what your middleware adds:
::: code-group
```ts [Koa: api/admin/use.ts]
import { use } from "_/api";
export type ExtendT = {
user: { id: number; role: "admin" | "user" };
};
export default [
use(async (ctx, next) => {
const token = ctx.headers.authorization?.replace("Bearer ", "");
// NOTE: validate before adding to state - ExtendT promises this property exists
ctx.assert(token, 401, "Authentication required");
ctx.state.user = await verifyToken(token);
return next();
})
];
```
```ts [Hono: api/admin/use.ts]
import { use } from "_/api";
export type ExtendT = {
user: { id: number; role: "admin" | "user" };
};
export default [
use(async (ctx, next) => {
const token = ctx.req.header("authorization")?.replace("Bearer ", "");
// NOTE: validate before adding to context - ExtendT promises this property exists
if (!token) throw new HTTPException(401, { message: "Authentication required" });
ctx.set("user", await verifyToken(token));
return next();
})
];
```
:::
Now every route under `/api/admin` has `user` typed on the context automatically -
no imports, no type arguments on `defineRoute`:
::: code-group
```ts [Koa: api/admin/dashboard/index.ts]
export default defineRoute<"admin/dashboard">(({ GET }) => [
GET(async (ctx) => {
const { user } = ctx.state; // typed as { id: number; role: "admin" | "user" }
}),
]);
```
```ts [Hono: api/admin/dashboard/index.ts]
export default defineRoute<"admin/dashboard">(({ GET }) => [
GET(async (ctx) => {
const user = ctx.get("user"); // typed as { id: number; role: "admin" | "user" }
}),
]);
```
:::
The code generator imports `ExtendT` from each `use.ts` in the hierarchy
and merges them into the context type for `defineRoute`. Inner definitions
override outer ones - just like at runtime, where inner middleware runs after
outer middleware and can overwrite context values.
**Note:** the global `api/use.ts` does not need to export `ExtendT`.
Even if it does, the export is ignored - global middleware operates on
`DefaultState` (Koa) or `DefaultVariables` (Hono) defined in `api/env.d.ts`.
`ExtendT` is for folder-level `use.ts` files only, where the types cascade
alongside the middleware itself.
> **Tip:** inner `use.ts` files can import `ExtendT` from outer ones, extend it, and re-export -
> avoiding duplicate type definitions across the hierarchy:
>
> ```ts [api/admin/settings/use.ts]
> import type { ExtendT as ParentT } from "../use";
>
> export type ExtendT = ParentT & {
> settingsAccess: "read" | "write";
> };
> ```
## 💼 Common Use Cases
### Authentication
::: code-group
```ts [Koa: api/admin/use.ts]
import { use } from "_/api";
export type ExtendT = {
user: { id: number; name: string; role: string };
};
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.role === "admin", 403, "Admin access required");
ctx.state.user = user;
return next();
})
];
```
```ts [Hono: api/admin/use.ts]
import { HTTPException } from "hono/http-exception";
import { use } from "_/api";
export type ExtendT = {
user: { id: number; name: string; role: string };
};
export default [
use(async (ctx, next) => {
const token = ctx.req.header("authorization")?.replace("Bearer ", "");
if (!token) throw new HTTPException(401, { message: "Authentication required" });
const user = await verifyToken(token);
if (user.role !== "admin") throw new HTTPException(403, { message: "Admin access required" });
ctx.set("user", user);
return next();
})
];
```
:::
Every route under `/api/admin` now requires admin auth. Route handlers can assume
the user is already validated (`ctx.state.user` for Koa, `ctx.get("user")` for Hono).
### Request Logging
::: code-group
```ts [Koa: api/payments/use.ts]
import { use } from "_/api";
export type ExtendT = {
requestId: string;
};
export default [
use(async (ctx, next) => {
const start = Date.now();
const requestId = crypto.randomUUID();
ctx.state.requestId = requestId;
console.log(`[${requestId}] ${ctx.method} ${ctx.path}`);
try {
await next();
} finally {
console.log(`[${requestId}] completed in ${Date.now() - start}ms`);
}
})
];
```
```ts [Hono: api/payments/use.ts]
import { use } from "_/api";
export type ExtendT = {
requestId: string;
};
export default [
use(async (ctx, next) => {
const start = Date.now();
const requestId = crypto.randomUUID();
ctx.set("requestId", requestId);
console.log(`[${requestId}] ${ctx.req.method} ${ctx.req.path}`);
await next();
console.log(`[${requestId}] completed in ${Date.now() - start}ms`);
})
];
```
:::
### Rate Limiting
::: code-group
```ts [Koa: api/public/use.ts]
import rateLimit from "koa-ratelimit";
import { use } from "_/api";
export type ExtendT = {};
export default [
use(
rateLimit({
driver: "memory",
db: new Map(),
duration: 60000,
max: 100,
})
)
];
```
```ts [Hono: api/public/use.ts]
import { rateLimiter } from "hono-rate-limiter";
import { use } from "_/api";
export type ExtendT = {};
export default [
use(
rateLimiter({
windowMs: 60000,
limit: 100,
keyGenerator: (ctx) => ctx.req.header("x-forwarded-for") ?? "",
}),
)
];
```
:::
## ⚠️ Parameter Availability
Cascading middleware runs for all routes in the hierarchy, including ones that don't
define the parameters you might expect:
```txt
api/users/
├── [id]/index.ts ← has 'id' param
├── index.ts ← NO 'id' param
└── use.ts
```
`ctx.params.id` is undefined for `/users`. Keep cascading middleware generic -
authentication, logging, rate limiting. Parameter-specific logic belongs in the route handler.
## 🎨 Multiple Middleware + Method Filtering
A single `use.ts` can define multiple functions, and each supports the `on` option:
::: code-group
```ts [Koa: api/users/use.ts]
import { use } from "_/api";
export type ExtendT = {
user: { id: number; name: string };
};
export default [
use(async (ctx, next) => {
console.log(`${ctx.method} ${ctx.path}`);
return next();
}),
use(
async (ctx, next) => {
const token = ctx.headers.authorization?.replace("Bearer ", "");
ctx.assert(token, 401, "Authentication required");
ctx.state.user = await verifyToken(token);
return next();
},
{ on: ["POST", "PUT", "PATCH", "DELETE"] },
),
use(async (ctx, next) => {
const start = Date.now();
await next();
ctx.set("X-Response-Time", `${Date.now() - start}ms`);
}),
];
```
```ts [Hono: api/users/use.ts]
import { HTTPException } from "hono/http-exception";
import { use } from "_/api";
export type ExtendT = {
user: { id: number; name: string };
};
export default [
use(async (ctx, next) => {
console.log(`${ctx.req.method} ${ctx.req.path}`);
return next();
}),
use(
async (ctx, next) => {
const token = ctx.req.header("authorization")?.replace("Bearer ", "");
if (!token) throw new HTTPException(401, { message: "Authentication required" });
ctx.set("user", await verifyToken(token));
return next();
},
{ on: ["POST", "PUT", "PATCH", "DELETE"] },
),
use(async (ctx, next) => {
const start = Date.now();
await next();
ctx.header("X-Response-Time", `${Date.now() - start}ms`);
}),
];
```
:::
---
---
url: /fetch/validation.md
description: >-
Automatic client-side validation with TypeBox schemas before network requests.
Use check, errors, errorMessage methods for form validation with performance
optimization patterns.
---
Fetch clients validate parameters and payload before making any network request -
using the exact same schemas as the server. Invalid data throws immediately, no round trip needed.
## 📋 Validation Schemas
Beyond automatic fetch validation, each client exposes `validationSchemas` for use directly in your UI -
ideal for real-time form feedback:
```ts
const { validationSchemas } = fetchClients["users"];
validationSchemas.params; // parameter validation
validationSchemas.json.POST; // JSON payload validation for POST
```
Each schema has four methods:
* **`check(data)`** - fast boolean check, safe to call on every keystroke
* **`errors(data)`** - returns `Array` with field-level detail; only call after `check` returns false
* **`errorMessage(data)`** - all errors as a single readable string
* **`errorSummary(data)`** - brief overview, e.g. `"2 validation errors found across 2 fields"`
`check` is cheap. `errors`, `errorMessage`, and `errorSummary` are heavier - gate them behind `check`.
## 🪆 Field Paths
Nested field errors use arrow notation: `"customer ➜ address ➜ city"`.
Match them with word-boundary regex to avoid false positives:
```ts
const emailError = errors.find(({ path }) => /\bemail\b/.test(path));
```
## ⚡ Per-Field Validation Performance
Schemas validate entire objects, not individual fields. This creates a subtle issue
when validating fields as users type: on a partially-filled form, `check` returns false
for missing required fields - not just the one you're testing - which triggers
unnecessary `errors()` calls on every keystroke.
The fix is to merge the actual field value into a fully-valid placeholder payload,
so `check` only fails when the field under test actually has a problem:
```ts
// Define a valid baseline - all required fields filled with values that pass all constraints.
// This is a one-time setup per form, not per keystroke.
const validPayload = { name: "Valid Name", email: "valid@example.com", age: 25 };
// On input event for "name" - override just that field
const payload = { ...validPayload, name: event.target.value };
if (!validationSchemas.json.POST.check(payload)) {
const nameError = validationSchemas.json.POST.errors(payload).find(e => e.path === "name");
// show nameError.message near the name field
}
```
Each field gets its own merge - `{ ...validPayload, email: event.target.value }` for email, and so on.
The placeholder values for other fields are never submitted anywhere,
they just keep `check` from firing false negatives.
Most forms don't need this. If you validate on submit rather than on input,
or your form has only a few fields, direct validation works fine.
It matters for complex forms with many required fields that validate in real time.
On submit, always validate the actual payload - not the merged one:
```ts
if (!validationSchemas.json.POST.check(actualPayload)) {
const errors = validationSchemas.json.POST.errors(actualPayload);
// surface all errors at once
return;
}
await useFetch.POST([], actualPayload);
```
---
---
url: /frontend/custom-templates.md
description: >-
Override default generated page components for specific routes using glob
pattern matching. Create specialized scaffolding for landing pages, admin
dashboards, and marketing sections in React, SolidJS, Vue and MDX source
folders.
---
Each framework generator supports template overrides for specific routes through
pattern-based matching. When a new page is created and its path matches a
configured pattern, the generator writes your custom template instead of the
default - useful for standardizing structure across landing pages, admin tools,
or any section requiring a consistent starting point.
Templates are particularly powerful for batch route generation. A common
scenario is scaffolding CRUD API routes for multiple database tables: create
one template capturing the standard boilerplate, define your routes, and each
generated file starts with the right structure ready to adapt - instead of
writing the same skeleton N times by hand.
## ⚙️ Configuration
Pass custom templates via generator options in your source folder's `kosmo.config.ts`:
```ts [kosmo.config.ts]
import { defineConfig, reactGenerator } from "@kosmojs/dev";
// [!code ++:8]
const landingTemplate = `
export default function Page() {
return (
Welcome
);
}`;
export default defineConfig({
generators: [
reactGenerator({
templates: { // [!code ++:4]
"landing/*": landingTemplate,
"marketing/**/*": landingTemplate,
},
}),
],
});
```
## 🎯 Pattern Syntax
Templates use glob-style patterns to match routes:
### Single-Depth Wildcard (`*`)
Matches routes at exactly one nesting level:
```ts
{ "landing/*": template }
```
**Matches:** `landing/home`, `landing/about`, `landing/[slug]`
**Excludes:** `landing/features/new` (too deep), `landing` (too shallow)
### Multi-Depth Wildcard (`**`)
Matches routes at any nesting depth:
```ts
{ "marketing/**/*": template }
```
**Matches:** `marketing/campaigns/summer`, `marketing/promo/2024/special`, `marketing/[id]/details`
### Exact Match
Targets a single specific route:
```ts
{ "products/list": template }
```
## 📊 Resolution Priority
When multiple patterns match, the first matching pattern wins:
```ts
generator({
templates: {
"landing/home": homeTemplate, // highest specificity
"landing/*": landingTemplate, // medium specificity
"**/*": fallbackTemplate, // lowest specificity
},
})
```
## 🔀 Parameter Compatibility
Templates work with all parameter types:
```ts
{
"users/[id]": userTemplate, // required parameter
"products/{category}": productTemplate, // optional parameter
"docs/{...path}": docsTemplate, // splat parameter
"shop/[category]/{sub}": shopTemplate, // combined
}
```
## 📝 Template Format
Templates are plain strings written to disk as component files. Each framework
has its own component structure:
::: code-group
```ts [React]
const customTemplate = `
import { useParams } from "react-router";
export default function Page() {
const params = useParams();
return (
`;
```
```mdx [MDX]
import { useParams } from "_/use";
# Custom Template
Route params: {JSON.stringify(useParams())}
```
:::
> **Vue templates** use Handlebars syntax for any dynamic content injected
> during generation. Avoid raw Vue interpolation {{"{{"}}}} inside template
> strings - wrap in quotes or escape as needed to prevent accidental
> Handlebars evaluation.
## ✨ Common Use Cases
### Landing & Marketing Pages
```ts
generator({
templates: {
"landing/**/*": landingTemplate,
"marketing/**/*": marketingTemplate,
"promo/**/*": promoTemplate,
},
})
```
### Admin Interfaces
```ts
generator({
templates: {
"admin/**/*": adminTemplate,
},
})
```
## 📄 Default Template Override
Routes without a matching pattern use the generator's built-in default, which
displays the route name as a placeholder. Replace it globally with:
```ts
generator({
templates: {
"**/*": myDefaultTemplate,
},
})
```
---
---
url: /frontend/data-preload.md
description: >-
Prefetch route data before components render using React Router's loader
pattern, SolidJS Router's preload pattern, and Vue Router's navigation guards.
Type-safe data availability derived from API endpoint definitions.
---
Preloading ensures data is ready before a component renders, eliminating
loading spinners for route-level data and creating seamless navigation
experiences. Each framework has its own mechanism - all integrate naturally
with `KosmoJS`'s generated fetch clients.
## 📡 API Endpoint
Start by creating an API endpoint that provides the data. The same endpoint
is used across all three frameworks:
```ts [api/users/data/index.ts]
import { defineRoute } from "_/api";
export default defineRoute<"users/data">(({ GET }) => [
GET<{ response: [200, "json", Data] }>(async (ctx) => {
ctx.body = await fetchUserData();
}),
]);
```
## 🔌 Page Integration
::: code-group
```tsx [React]
import { useLoaderData } from "react-router";
import fetchClients, { type ResponseT } from "_/fetch";
const { GET } = fetchClients["users/data"];
// Export the fetch function as loader -
// React Router calls it before the component renders
export { GET as loader };
export default function Page() {
// useLoaderData retrieves the already-fetched result - no duplicate request
const data = useLoaderData();
return (
{data && }
);
}
```
```tsx [SolidJS]
import { createAsync } from "@solidjs/router";
import fetchClients from "_/fetch";
const { GET } = fetchClients["users/data"];
// Export the fetch function as preload -
// SolidJS Router calls it on link hover and navigation intent
export { GET as preload };
export default function Page() {
// createAsync recognizes GET as the preloaded function
// and reuses the cached result - no duplicate request
const data = createAsync(GET);
return (
{data() && }
);
}
```
```vue [Vue]
```
:::
## 🔍 How It Works
**React** - the `loader` export tells React Router what function to call
before rendering. `useLoaderData` retrieves the result that was already
fetched - no duplicate request. Type safety flows end-to-end: the fetch
client's `GET` is typed from your API definition, and `useLoaderData` is
parameterized with the matching response type.
**SolidJS** - the `preload` export tells SolidJS Router to call the function
on link hover and navigation intent. `createAsync` receives the same function
reference and recognizes it as already-cached data, reusing it without
re-fetching. Type inference is automatic - `createAsync` infers its return
type directly from the function signature.
**Vue** - Vue Router has no built-in route-level preload mechanism. The
idiomatic approach is `onMounted` for initial data, `watch` on route params
for reactive updates, or navigation guards in `router.ts` for blocking
pre-fetch before the component mounts. See the
[Vue Router documentation](https://router.vuejs.org/guide/advanced/data-fetching.html)
for the full range of options.
---
---
url: /backend/development-workflow.md
description: >-
Run multiple KosmoJS source folders independently with separate dev servers,
automatic API hot-reload, custom middleware routing, and resource cleanup with
teardown handlers.
---
Each source folder serves a specific concern - marketing site, customer app, admin, etc.
Yet, development workflow is identical.
## 🚀 Starting the Dev Server
```sh
pnpm dev # all source folders
pnpm dev front # specific folder (front, admin, app, etc.)
```
Default port is `4556`, configured as `devPort` in `package.json`.
## 🔀 What Happens on Start
1. `Vite` compiles `api/app.ts`
2. Dev server starts, serving both client pages and your API routes
3. Requests are routed between Vite and your API
4. File watcher monitors API files for changes
## ⚙️ api/dev.ts
`api/dev.ts` exposes three hooks for customizing the dev experience.
### requestHandler
Returns the API request handler. Generated default:
::: code-group
```ts [Koa]
import { devSetup } from "_/api:factory";
import app from "./app";
export default devSetup({
requestHandler() {
return app.callback();
},
});
```
```ts [Hono]
import { getRequestListener } from "@hono/node-server";
import { devSetup } from "_/api:factory";
import app from "./app";
export default devSetup({
requestHandler() {
return getRequestListener(app.fetch);
},
});
```
:::
Override this for custom routing logic - WebSocket handling, multi-handler dispatch, etc.
### requestMatcher
Controls which requests go to your API vs Vite. Defaults to matching `apiurl` prefix:
```ts
export default devSetup({
requestHandler() { return app.callback(); },
requestMatcher(req) {
return req.url?.startsWith("/api") ||
req.headers["x-api-request"] === "true";
},
});
```
### teardownHandler
Runs before each API reload. Use it to close connections and release resources
that would otherwise leak across rebuilds:
```ts
let dbConnection;
export default devSetup({
requestHandler() { return app.callback(); },
async teardownHandler() {
if (dbConnection) {
await dbConnection.close();
dbConnection = undefined;
}
},
});
```
Without cleanup, frequent rebuilds during active development can exhaust database connections.
## 👀 Inspecting Routes
Each route returned by `createRoutes` has a `debug` property. Enable it via `DEBUG=api`:
```ts [api/router.ts]
import { routerFactory, routes } from "_/api:factory";
const DEBUG = /\bapi\b/.test(process.env.DEBUG ?? ""); // [!code ++]
export default routerFactory(({ createRouter }) => {
const router = createRouter();
for (const { name, path, methods, middleware, debug } of routes) {
if (DEBUG) console.log(debug.full); // [!code ++]
router.register(path, methods, middleware, { name });
}
return router;
});
```
```sh
DEBUG=api pnpm dev
```
Example output:
```txt
/api/users [ users/index.ts ]
methods: POST
middleware: slot: params; exec: useParams
slot: validateParams; exec: useValidateParams
slot: bodyparser; exec: async (ctx, next) => {
slot: payload; exec: (ctx, next) => {
handler: postHandler
```
Named middleware functions show by name; anonymous ones show their first line.
Name your middleware functions - it makes this output significantly easier to read.
Individual `debug` properties are also available for targeted output:
`debug.headline`, `debug.methods`, `debug.middleware`, `debug.handler`.
---
---
url: /routing/rationale.md
description: >-
Understanding why directory-based routing scales better than file-based
routing for organizing large applications with clear navigation,
colocalization, and visual hierarchy.
---
At first glance, directory-based routing looks more verbose than file-based alternatives.
`api/users/[id]/index.ts` vs `api/users/[id].ts` - the extra folder seems unnecessary.
It isn't, and the reason becomes obvious as your project grows.
## ⚠️ The File-Based Routing Problem
In file-based routing, route handlers and helper files live side by side:
```
api/
users/
index.ts ➜ Handler for /users
[id].ts ➜ Handler for /users/:id
schema.ts ➜ Validation schemas... for which route?
auth.ts ➜ Authorization... for which endpoint?
utils.ts ➜ Helpers... used by what?
```
Which files are route handlers? Which are helpers? Is `schema.ts` a route at `/users/schema`
or a shared validation file? You can't tell without opening each file or relying on team conventions.
## 🏆 Directory-Based Clarity
With directory-based routing, the rule is simple: **only `index.ts` is a route handler**.
Everything else in the folder is a helper for that route.
```
api/
users/
index.ts ➜ Handler for /users
schema.ts ➜ Obviously a helper for /users
[id]/
index.ts ➜ Handler for /users/:id
permissions.ts ➜ Obviously a helper for this endpoint
posts/
index.ts ➜ Handler for /users/:id/posts
formatter.ts ➜ Obviously post-specific logic
```
No conventions to memorize, no ambiguity. The folder tree is your API map -
every folder with an `index.ts` is a route, everything else is support code.
This scales naturally:
```
api/
products/
index.ts
[id]/
index.ts
cache.ts
pricing.ts
reviews/
index.ts
moderation.ts
[reviewId]/
index.ts
flags.ts
```
Each route's complexity is isolated in its own folder.
New developers understand the structure immediately.
Six months later, you can still navigate it without re-reading the codebase.
## ⚖️ The Trade-off
You create a folder even when it only contains `index.ts`. That's the entire cost.
In return: zero ambiguity, natural colocalization, room to grow without restructuring,
and a folder tree that directly mirrors your API surface:
```sh
$ tree -d src/front/api
src/front/api/
└── shop
├── cart
├── [category]
│ └── {productId}
├── checkout
│ ├── confirm
│ ├── payment
│ └── shipping
├── orders
│ └── [orderId]
└── products
└── {category}
```
---
---
url: /routing/intro.md
description: >-
KosmoJS uses directory-based routing to map file system structure directly to
URL paths. Folder names become path segments with index files defining
endpoints and components.
---
`KosmoJS` uses directory-based routing: folder names become URL path segments,
and `index` files define the actual endpoints or components.
No separate routing configuration - your file structure is your route definition.
## 🛣️ How It Works
The same pattern applies to both API routes and client pages:
```
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
```
The parallel structure between `api/` and `pages/` is intentional -
if you have a `/users/[id]` page, the corresponding `/api/users/[id]` endpoint is easy to find.
Every route lives in a folder, including the root - the base route uses a folder named `index`.
This consistency means no special cases: every route is a folder with an `index` file inside.
## 📄 Route File Requirements
API routes export a route definition (HTTP methods + handlers).
Client pages export a component function.
The [auto-generation feature](/routing/generated-content) produces the correct boilerplate
when you create a new file, so you rarely write it from scratch.
The folder-per-route pattern gives each route its own namespace for colocating related files -
utilities, types, tests - without cluttering parent directories.
## 🏗️ Nested Routes
Nesting works by nesting folders. `api/users/[id]/posts/index.ts` maps to `/api/users/:id/posts`,
and can go as deep as your domain requires. Each level can colocate its own helpers,
types, and tests without affecting siblings.
For client pages, nested routes support layout components that wrap child routes
with shared UI like navigation or headers.
([Details ➜ ](/frontend/routing))
---
---
url: /routing/params.md
description: >-
Handle dynamic URL segments with required [id], optional {id} and splat
{...path} parameters. SolidStart-inspired syntax that works identically for
API routes and client pages.
---
`KosmoJS` supports three parameter types, using the same syntax for both API routes and client pages:
| Syntax | Type | Matches |
|---|---|---|
| `[id]` | Required | Exactly one segment |
| `{id}` | Optional | One segment or nothing |
| `{...path}` | Splat | Any number of segments |
## \[] Required Parameters
```
users/[id]/index.ts ➜ /users/123, /users/abc
```
The parameter name becomes the key in `ctx.validated.params` (or your framework's equivalent).
`[id]` gives you `params.id`, `[userId]` gives you `params.userId`.
## {} Optional Parameters
```
users/{id}/index.ts ➜ /users and /users/123
```
Useful for combining list and detail views in a single handler,
branching on whether the parameter is present.
**Important:** optional parameters must not be followed by required parameters.
```
users/{section}/{subsection} ✅
users/{optional}/[required] ❌
```
### Watch Out for Ambiguous Paths
Optional parameters followed by static segments can cause unexpected 404s:
```
properties/{city}/filters/index.tsx
```
Visiting `/properties/filters`: the router matches `{city}` = `"filters"`,
then expects another `/filters` segment - which isn't there. Result: 404.
Fix it by adding an explicit static route:
```
properties/
├── filters/index.tsx ➜ /properties/filters
└── {city}/
└── filters/index.tsx ➜ /properties/NY/filters
```
Static routes always take priority over dynamic ones.
### Required vs Optional - a Subtlety
`[id]` technically means "required at this URL position", but a sibling `index` file changes that:
```
careers/
├── index.tsx ➜ /careers (fallback when no id)
└── [jobId]/
└── index.tsx ➜ /careers/123
```
When a parent `index` exists, `[jobId]` is effectively optional - there's a fallback to render.
In this case, `{jobId}` communicates intent more clearly and both notations work identically.
## {...} Splat Parameters
```
docs/{...path}/index.ts ➜ /docs/getting-started
➜ /docs/api/reference
➜ /docs/guides/deployment/production
```
The matched segments are provided as an array - useful for doc sites, file browsers,
or anything with arbitrarily nested paths.
A request to `/docs/guides/deployment/production` gives you
`params.path` as `["guides", "deployment", "production"]`.
## 🔗 Mixed Segments
Segments can combine static text with parameters:
```
products/[category].html ➜ /products/electronics.html
profiles/[id]-[data].json ➜ /profiles/1-posts.json
files/[name].[ext] ➜ /files/document.pdf
```
Mixed segments work fully for backend routes (Koa/Hono). Frontend support varies:
* **Vue Router** - full support
* **MDX** - full support
* **React Router** - `.ext` suffix only
* **SolidJS Router** - not supported
Prefer simple segments for frontend routes.
## ⚡ Power Syntax
For advanced cases, `KosmoJS` passes `path-to-regexp v8` patterns through directly.
**The rule:** if the param name contains non-alphanumeric characters, it's treated as a raw pattern.
This unlocks things like optional static parts:
```
products/{:category.html}
```
* `/products` ✅ (no category, no `.html`)
* `/products/electronics.html` ✅
* `/products/electronics` ❌ (`.html` required when category is present)
More examples:
```
book{-:id}-info ➜ /book-info or /book-123-info
locale{-:lang{-:country}} ➜ /locale, /locale-en, /locale-en-US
api/{v:version}/users ➜ /api/users or /api/v2/users
```
Use power syntax carefully - read the [path-to-regexp docs](https://github.com/pillarjs/path-to-regexp) before applying it to production routes.
---
---
url: /backend/context.md
description: >-
Learn about KosmoJS's enhanced context with unified bodyparser API and
ctx.validated for type-safe validated data access
---
`KosmoJS` extends the standard Koa/Hono context with two additions:
a unified bodyparser API and `ctx.validated` for type-safe access to validated request data.
## 🔋 Unified Bodyparser
`ctx.bodyparser` works the same regardless of framework:
```ts
await ctx.bodyparser.json() // JSON request body
await ctx.bodyparser.form() // URL-encoded or multipart form
await ctx.bodyparser.raw() // raw body buffer
```
Results are cached - calling the same parser multiple times doesn't re-parse the request.
In practice you rarely call this directly. Define a validation schema in your handler
and the appropriate parser runs automatically, placing the result in `ctx.validated`.
## ☔ Validated Data Access
`ctx.validated` holds the validated, typed result for each target you defined:
```ts
export default defineRoute(({ POST }) => [
POST<{
json: Payload,
query: { limit: number },
headers: { "x-api-key": string },
}>(async (ctx) => {
const user = ctx.validated.json; // validated JSON body
const limit = ctx.validated.query; // validated query params
const apiKey = ctx.validated.headers; // validated headers
}),
]);
```
## 🔗 Route Parameters
Validated params are available at `ctx.validated.params`, typed according to your refinements:
```ts [api/users/[id]/index.ts]
export default defineRoute<"users/[id]", [number]>(({ GET, POST }) => [
GET<{
query: { page: string; filter?: string },
}>(async (ctx) => {
const { id } = ctx.validated.params; // number
const { page, filter } = ctx.validated.query;
}),
POST<{
json: Payload,
}>(async (ctx) => {
const { id } = ctx.validated.params; // number
const user = ctx.validated.json;
}),
]);
```
The underlying `ctx.params` (Koa) and `ctx.req.param()` (Hono) still exist if you need the raw strings.
---
---
url: /backend/error-handling.md
description: >-
Handle errors gracefully in KosmoJS with customizable error handlers for Koa
and Hono. Learn about default error handling, route-level overrides, and
framework differences.
---
`KosmoJS` generates `api/errors.ts` file with a working default error handler when you create a source folder.
It's a regular file - customize it freely.
## 📦 Default Error Handler
::: code-group
```ts [Koa: api/errors.ts]
import { ValidationError } from "@kosmojs/core/errors";
import { errorHandlerFactory } from "_/api:factory";
export default errorHandlerFactory(
async function defaultErrorHandler(ctx, next) {
try {
await next();
} catch (error: any) {
const [errorMessage, status] =
error instanceof ValidationError
? [`${error.target}: ${error.errorMessage}`, 400]
: [error.message, error.statusCode || 500];
if (ctx.accepts("json")) {
ctx.status = status;
ctx.body = { error: errorMessage };
} else {
ctx.status = status;
ctx.body = errorMessage;
}
}
},
);
```
```ts [Hono: api/errors.ts]
import { accepts } from "hono/accepts";
import { HTTPException } from "hono/http-exception";
import { ValidationError } from "@kosmojs/core/errors";
import { errorHandlerFactory } from "_/api:factory";
export default errorHandlerFactory(
async function defaultErrorHandler(error, ctx) {
// HTTPException knows how to render itself
if (error instanceof HTTPException) {
return error.getResponse();
}
const [message, status] =
error instanceof ValidationError
? [`${error.target}: ${error.errorMessage}`, 400]
: [error.message, error.statusCode || 500];
const type = accepts(ctx, {
header: "Accept",
supports: ["application/json", "text/plain"],
default: "text/plain",
});
return type === "application/json"
? ctx.json({ error: message }, status)
: ctx.text(message, status);
},
);
```
:::
The Koa handler is wired into global middleware at `api/use.ts` via `errorHandler` slot.
The Hono handler is wired into `app.onError()` in the `api/app.ts`.
## 🎨 Customization
Add logging, monitoring, or structured error responses directly in `api/errors.ts`:
::: code-group
```ts [Koa]
async function defaultErrorHandler(ctx, next) {
try {
await next();
} catch (error: any) {
ctx.app.emit("error", error, ctx); // [!code ++]
// ... rest of error handling
}
}
```
```ts [Hono]
async function defaultErrorHandler(error, ctx) {
console.error(`[${ctx.req.method}] ${ctx.req.path}:`, error); // [!code ++]
await reportToSentry(error); // [!code ++]
if (error instanceof HTTPException) return error.getResponse();
// ... rest of error handling
}
```
:::
For Koa, you can listen to app-level error events in `api/app.ts`:
```ts [api/app.ts]
export default appFactory(({ createApp }) => {
const app = createApp();
app.on("error", (error) => { // [!code focus:3]
console.error("API Error:", error);
});
app.use(router.routes());
return app;
});
```
## 🎯 Route-Level Overrides (Koa)
Koa supports overriding the error handler per-route or per-subtree via `errorHandler` slot:
```ts [api/webhooks/github/index.ts]
export default defineRoute(({ use, POST }) => [
use(async (ctx, next) => {
try {
await next();
} catch (error: any) {
ctx.status = error.statusCode || 500;
ctx.body = error.message; // plain text for webhooks
}
}, { slot: "errorHandler" }), // [!code hl]
POST(async (ctx) => { /* ... */ }),
]);
```
For multiple routes, use a cascading `use.ts`:
```ts [api/webhooks/use.ts]
export default [
use(async (ctx, next) => {
try {
await next();
} catch (error: any) {
ctx.status = error.statusCode || 500;
ctx.body = error.message;
console.error(`Webhook error: ${ctx.path}`, error);
}
}, { slot: "errorHandler" }),
];
```
Hono has a single `app.onError()` for the entire application - branch on `ctx.req.path`
inside the handler if you need route-specific behavior.
## 🔄 Let Handlers Fail
Don't wrap handler logic in try-catch. Let errors propagate to the error handler:
::: code-group
```ts [Koa]
// ❌ unnecessary
GET(async (ctx) => {
try {
const user = await fetchUser(ctx.params.id);
ctx.body = user;
} catch (error) {
ctx.status = 500;
ctx.body = { error: "Failed to fetch user" };
}
});
// ✅ use ctx.assert / ctx.throw
GET(async (ctx) => {
const user = await fetchUser(ctx.params.id);
ctx.assert(user, 404, "User not found");
ctx.body = user;
});
```
```ts [Hono]
// ❌ unnecessary
GET(async (ctx) => {
try {
const user = await fetchUser(ctx.validated.params.id);
return ctx.json(user);
} catch (error) {
return ctx.json({ error: "Failed to fetch user" }, 500);
}
}),
// ✅ use HTTPException
GET(async (ctx) => {
const user = await fetchUser(ctx.validated.params.id);
if (!user) throw new HTTPException(404, { message: "User not found" });
return ctx.json(user);
}),
```
:::
## ⚡ Koa vs Hono - Key Differences
| | Koa | Hono |
|---|---|---|
| Error model | Middleware try-catch, bubbles up | `app.onError()` catches everything |
| `await next()` throws? | Yes | No |
| Response style | Mutate `ctx.body` / `ctx.status` | Return a `Response` |
| Per-route override | `errorHandler` slot | Branch inside `app.onError()` |
---
---
url: /features.md
description: >-
Explore KosmoJS features including multiple source folders, directory-based
routing, end-to-end type safety, generated fetch clients, OpenAPI specs, and
framework freedom - Koa, Hono, React, Solid, Vue, MDX.
---
Everything `KosmoJS` provides, at a glance.
## 🗂️ Multiple Source Folders
Organize distinct concerns - public site, customer app, admin dashboard -
as independent source folders within a single `Vite` project.
Each gets its own set of frameworks, base URL, development workflow and build pipeline.
[Read more ➜](/start#📁-create-your-first-source-folder)
## 🛣️ Directory-Based Routing
Your folder structure defines your routes - for both API and client pages.
```
api/users/[id]/index.ts ➜ /api/users/:id
pages/users/[id]/index.tsx ➜ /users/:id
```
Dynamic parameters: `[id]` required · `{id}` optional · `{...path}` splat.
No separate routing config to maintain - restructure files and routes update automatically.
Mixed segments are also supported for backend routes (and some frontend integrations):
```
products/[category].html/index.ts ➜ products/electronics.html
files/[name].[ext]/index.ts ➜ files/document.pdf, /files/logo.png
```
[Read more ➜](/routing/intro)
## ⚡ Power Syntax for Params
When standard named parameters aren't enough, use raw [path-to-regexp v8](https://github.com/pillarjs/path-to-regexp)
patterns directly in your folder names:
```
book{-:id}-info ➜ /book-info or /book-123-info
locale{-:lang{-:country}} ➜ /locale, /locale-en, /locale-en-US
api/{v:version}/users ➜ /api/users or /api/v2/users
```
Any param name containing non-alphanumeric characters
is treated as a raw pattern - giving you precise control over URL structure
without sacrificing the directory-based routing model.
[Read more ➜](/routing/params#power-syntax)
## 🛡️ End-to-End Type Safety
Write `TypeScript` types once - `KosmoJS` generates runtime validators automatically.
The same definition drives compile-time checking, runtime validation, type-safe fetch clients, and OpenAPI specs.
```ts
export default defineRoute(({ POST }) => [
POST<{
json: {
email: VRefine;
age: VRefine;
},
response: [200, "json", User],
}>(async (ctx) => {
const { email, age } = ctx.validated.json;
// payload validated before reaching here
// response validated before sending
}),
]);
```
[Read more ➜](/validation/intro)
## 🔗 Generated Fetch Clients + OpenAPI
For every API route, `KosmoJS` generates a fully-typed fetch client
and an OpenAPI 3.1 spec - both derived from the same type definitions.
```ts
import fetchClients from "_/fetch";
const user = await fetchClients["users/[id]"].GET([123]);
// fully typed, validates payload client-side before the request is sent
```
[Fetch Clients ➜](/fetch/intro) · [OpenAPI ➜](/openapi)
## 🎛️ Composable Middleware (Slots)
Global middleware defined in `api/use.ts` can be overridden per-route or per-subtree
using named slots - without removing or bypassing parent middleware entirely.
```ts
// global default in api/use.ts
use(async (ctx, next) => { /* ... */ }, { slot: "logger" })
// override for a specific route
use(async (ctx, next) => { /* custom logger */ }, { slot: "logger" })
```
Slots give you surgical control over middleware composition:
replace only what needs replacing, inherit everything else.
Custom slot names are supported by extending the `UseSlots` interface.
[Read more ➜](/backend/middleware)
## 🌊 Cascading Middleware
Place a `use.ts` file in any folder and its middleware automatically wraps
all routes in that folder and its subfolders - no imports or wiring needed.
```
api/admin/use.ts → wraps all routes under /api/admin
api/admin/users/use.ts → wraps only routes under /api/admin/users
```
Parent middleware always runs before child middleware.
Combine with slots to override globals for entire route subtrees.
[Read more ➜](/backend/cascading-middleware)
## 🪆 Nested Layouts
Frontend pages support nested layout components that wrap child routes -
compose shared UI (nav, sidebars, auth shells) at any level of the route hierarchy.
```
pages/
app/
layout.tsx ← wraps all /app/* pages
dashboard/
layout.tsx ← wraps all /app/dashboard/* pages
index.tsx
settings/
index.tsx
```
[Read more ➜](/frontend/routing)
## 🎨 Multiple Frameworks
**Backend:** `Koa` or `Hono` - same routing architecture, same type safety.
**Frontend:** `React`, `Vue`, `SolidJS`, `MDX` - same routing/layout/SSR conventions.
Different source folders can use different framework combinations.
When you add a source folder, `KosmoJS` generates a ready-to-go setup for your chosen stack -
router config, entry points, TypeScript settings, and all the wiring between them.
Switch frameworks per folder without learning a new set of conventions.
[Read more ➜](/frontend/intro)
## 🔧 Built on Proven Tools
No proprietary runtime, no custom bundler, no framework lock-in.
Every layer is a tool you can use, debug, and replace independently.
***
---
---
url: /fetch/error-handling.md
description: >-
Handle fetch request errors with try-catch blocks, distinguish ValidationError
from network errors, and implement defense in depth with client-side and
server-side validation.
---
The fetch client throws two distinct error types worth handling separately:
`ValidationError` for failed validation before the request is sent,
and standard errors for network or server failures.
```ts [pages/example/index.tsx]
import fetchMap, { ValidationError } from "_/fetch";
const useFetch = fetchMap["users/[id]"];
try {
const response = await useFetch.POST([userId], payload);
} catch (error) {
if (error instanceof ValidationError) {
// data didn't pass validation - no request was made
console.error("Invalid data:", error.message);
} else {
// network error, server error, etc.
console.error("Request failed:", error);
}
}
```
Validation errors carry the same structured detail as server-side `ValidationError` instances -
`target`, `errors`, `errorMessage`, `errorSummary` - so you can surface field-level feedback
without waiting for a server response.
[More on Error Details →](/validation/error-handling)
---
---
url: /fetch/integration.md
description: >-
Integrate KosmoJS fetch clients with SolidJS createResource, React hooks, and
custom state management patterns. Type safety flows through all framework
abstractions.
---
The fetch client returns standard promises, so it fits naturally into whatever async pattern your framework uses.
::: code-group
```ts [SolidJS]
import { createResource } from "solid-js";
import fetchClients from "_/fetch";
const { GET } = fetchClients["users/[id]"];
function UserProfile(props) {
const [user] = createResource(() => props.userId, (id) => GET([id]));
return (
{user.loading &&
Loading...
}
{user() &&
{user().name}
}
);
}
```
```ts [React]
import { useState, useEffect } from "react";
import fetchClients from "_/fetch";
const { GET } = fetchClients["users/[id]"];
function useUser(userId: number) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
GET([userId])
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, [userId]);
return { user, loading, error };
}
```
:::
Types flow through these abstractions - hooks and components automatically know the response shape
from your API definition.
---
---
url: /frontend/intro.md
description: >-
Integrate KosmoJS directory-based routing with React, SolidJS, Vue, or MDX.
Automatic route configuration, type-safe navigation, and optimized lazy
loading for modern frontend applications.
---
`KosmoJS` provides dedicated generators for `React`, `SolidJS`, `Vue` and `MDX` -
each bridging directory-based routing with the framework's native router and
reactive model. Your page components automatically become navigable routes
with full type safety and efficient code-splitting, while generated utilities
integrate naturally with each framework's patterns.
## 🛠️ Enabling the Generator
Framework generators are automatically enabled when creating a source folder
and selecting your framework. To add one to an existing folder, register it
manually in your source folder's `kosmo.config.ts`:
```ts [kosmo.config.ts]
import reactPlugin from "@vitejs/plugin-react"; // [!code ++]
import {
defineConfig,
// ...
reactGenerator, // [!code ++]
} from "@kosmojs/dev";
export default defineConfig({
// ...
plugins: [
reactPlugin(), // [!code ++]
],
generators: [
// ...
reactGenerator(), // [!code ++]
],
});
```
After configuration, the generator deploys essential files to your source
folder, establishing the application foundation.
## 🗂️ Multi-Folder Architecture
Projects spanning multiple source folders give each folder its own generator
instance with independent configuration. Generated types and utilities are
scoped per folder - routes in your main application won't appear in the admin
dashboard's navigation types, and vice versa.
Despite operating in separate namespaces, all source folders share `KosmoJS`'s
foundational conventions, ensuring consistency where it matters.
## 💡 TypeScript Configuration
Mixing frameworks across source folders requires per-folder TypeScript
configuration. Each framework has its own JSX import source requirement:
| Framework | `jsxImportSource` |
|-----------|-------------------|
| React | `"react"` |
| SolidJS | `"solid-js"` |
| Vue | `"vue"` *(only when using JSX)* |
| MDX | `"preact"` |
All frameworks use `jsx: "preserve"` - `KosmoJS` delegates JSX transformation
to Vite, not TypeScript - but differing `jsxImportSource` values cause type
conflicts when multiple frameworks coexist in the same project.
`KosmoJS` solves this by generating a `tsconfig.base.json` specific to each source folder,
placed in the `lib/` directory for the source folder to extend:
```json [src/front/tsconfig.json]
{ "extends": "../../lib/front/tsconfig.base.json" }
```
Each config supplies the correct `jsxImportSource`, path mappings, and core settings.
---
---
url: /fetch/intro.md
description: >-
KosmoJS automatically generates fully-typed fetch clients with runtime
validation for every API route. End-to-end type safety from frontend to
backend with validation schemas and URL utilities.
---
When you define an API route with typed parameters, payloads, and responses,
`KosmoJS` generates a corresponding fetch client - automatically, as part of the same build step.
The result is a fully-typed client that mirrors your route definition exactly.
Parameters, payload shape, response type - all derived from the same source.
Change your API, and the client updates with it. No manual sync required.
## 🤖 What Gets Generated
Each route's client module exports:
🔹 **HTTP method functions** - `GET`, `POST`, `PUT`, etc., accepting parameters and payloads
typed to match your route definition and returning typed response promises.
([Details ➜ ](/fetch/start))
🔹 **`path` and `href` utilities** - construct relative or absolute URLs with proper
parameter substitution and optional query string support.
([Details ➜ ](/fetch/utilities))
🔹 **`validationSchemas`** - the same schemas used for server-side validation,
exposed for client-side form validation with `check`, `errors`, `errorMessage`,
`errorSummary`, and `validate` methods.
([Details ➜ ](/fetch/validation))
## 🏗️ Using the Generated Client
Import the fetch map and pick the client for your route by path:
```ts [pages/example/index.tsx]
import fetchClients from "_/fetch";
const response = await fetchClients["users/[id]"].GET([123]);
```
The generator places its output in the `lib` directory alongside other generated artifacts
(validation routines, OpenAPI spec). Everything is updated automatically in the background
as you modify routes during development.
---
---
url: /fetch/start.md
description: >-
Import and use KosmoJS generated fetch clients with full TypeScript typing.
Access routes directly or through a centralized map with automatic parameter
and payload validation.
---
The fetch index exports a map of route paths to their generated clients:
```ts [pages/example/index.tsx]
import fetchClients from "_/fetch";
const response = await fetchClients["users/[id]"].GET([123]);
```
## 🚀 Method Signatures
Each client exposes methods for the HTTP verbs your route handles.
The signature reflects your route definition directly:
* First argument is a parameter array, in path order
* Second argument is the payload, if your handler defines one
Given this route:
```ts [api/users/[id]/index.ts]
export default defineRoute<"users/[id]", [number]>(({ GET }) => [
GET<{
query: { name?: string },
response: [200, "json", { id: number; name: string; email: string }],
}>(async (ctx) => { /* ... */ }),
]);
```
The generated client expects a number parameter and an optional payload:
```ts [pages/example/index.tsx]
const useFetch = fetchClients["users/[id]"];
const response = await useFetch.GET([123]);
const response = await useFetch.GET([123], { query: { name: "John" } });
// response is typed as { id: number; name: string; email: string }
```
## 📭 Routes Without Parameters or Payloads
No parameters, no array:
```ts
const response = await fetchClients["users"].GET();
```
If there is a payload to send without params, just use an empty array for params:
```ts
const response = await fetchClients["users"].GET([], {
query: { filter: "active", page: 1 }
});
```
If the route defines no payload type (or `never`), the second argument is not required.
The client adapts to exactly what your API expects - passing the wrong shape is a type error.
---
---
url: /frontend/mdx.md
description: >-
Create content-focused source folders with MDX - static HTML rendering with
Preact, nested layouts, frontmatter-driven head injection, typed navigation,
and optional static site generation. No client-side JavaScript by default.
---
MDX source folders are purpose-built for content: documentation, blogs,
marketing pages, and any site where prose matters more than interactivity.
Pages are authored in MDX (Markdown with JSX), rendered to static HTML on the
server with Preact, and delivered with minimal client-side JavaScript by default.
The same directory-based routing, nested layouts, and type-safe navigation
used by React, SolidJS, and Vue source folders apply for MDX as well.
## 🛠️ Enabling the Generator
MDX generator is automatically enabled when creating a source folder and
selecting MDX as the framework. To add one to an existing folder:
```ts [kosmo.config.ts]
import {
// ...
mdxGenerator, // [!code ++]
} from "@kosmojs/dev";
import frontmatterPlugin from "remark-frontmatter"; // [!code ++:2]
import mdxFrontmatterPlugin from "remark-mdx-frontmatter";
export default defineConfig({
// ...
generators: [
// ...
mdxGenerator({ // [!code ++:3]
remarkPlugins: [frontmatterPlugin, mdxFrontmatterPlugin]
}),
],
});
```
## 📄 Writing Pages
Pages are `.mdx` or `.md` files in your `pages/` directory.
Standard markdown syntax works alongside JSX components:
```mdx [pages/blog/index.mdx]
---
title: Blog
description: Latest posts and updates.
---
import Alert from "./Alert.tsx"
# Welcome to the Blog
Regular markdown works as expected - **bold**, *italic*, `code`,
[links](/about), and everything else.
JSX components work inline with markdown content.
## Recent Posts
- First post about KosmoJS
- Getting started with MDX
```
Frontmatter is defined in YAML between `---` fences.
It drives `` injection and is accessible to layouts via props.
## 🧩 Using Components
Import Preact components directly into MDX files. TypeScript, props, hooks -
everything works in the `.tsx` file. The MDX file stays focused on content:
::: code-group
```tsx [pages/blog/Alert.tsx]
import type { JSX } from "preact";
export default function Alert(props: {
type: "info" | "warning" | "error";
children: JSX.Element;
}) {
return (
{props.children}
);
}
```
```mdx [pages/blog/index.mdx]
import Alert from "./Alert.tsx"
Keep TypeScript in `.tsx` files - MDX only supports plain JavaScript.
```
:::
### Global Component Overrides
Every markdown element (`# heading`, `` `code` ``, `[link](url)`) compiles to
a JSX call. Override any of them globally via the component map in
`components/mdx.tsx`:
```tsx [src/components/mdx.tsx]
import Link from "./Link";
export const components = {
Link,
// custom heading with anchor links
h1: (props) => (
{props.children}
),
// syntax-highlighted code blocks
pre: (props) => ,
};
```
These overrides apply to all MDX pages via the `MDXProvider`.
Individual pages can still import and use additional components directly.
## 🪆 Layouts
Layouts work identically to other frameworks -
a `layout.mdx` file wraps all pages and nested layouts within its folder:
```txt
pages/
├── index/
│ └── index.mdx ← wrapped by root layout
├── docs/
│ ├── layout.mdx ← wraps all docs/* pages
│ ├── links/
│ │ └── index.mdx ← wrapped by root + docs layout
│ └── guide/
│ ├── layout.mdx ← wraps all docs/guide/* pages
│ └── setup/
│ └── index.mdx ← wrapped by root + docs + guide layout
```
For `/docs/guide/setup` the render order is:
```
App.mdx (root layout)
└── pages/docs/layout.mdx
└── pages/docs/guide/layout.mdx
└── pages/docs/guide/setup/index.mdx
```
### Writing Layouts
Layouts receive `props.children` (the wrapped content) and
`props.frontmatter` (from the matched page):
```mdx [pages/docs/layout.mdx]
{props.children}
```
Access the page's frontmatter for dynamic head content or conditional rendering:
```mdx [pages/layout.mdx]
{props.frontmatter.title && (
{props.frontmatter.title}
)}
{props.children}
```
Layouts must be `.mdx` files - `.md` files cannot render `{props.children}`.
### Global Layout via App.mdx
`App.mdx` at the source folder root wraps every page - the right place
for truly global concerns like site-wide navigation, footer, or
analytics scripts:
```txt
src/content/
├── App.mdx ← wraps everything
└── pages/
├── layout.mdx
└── index/
└── index.mdx
```
## 🛣️ Route Parameters
MDX pages support the same parameter syntax as other source folders:
```txt
pages/
blog/
post/
[slug]/
index.mdx ➜ /blog/post/:slug
{category}/
index.mdx ➜ /blog/:category (optional)
{tag}/
index.mdx ➜ /blog/:category/:tag (both optional)
```
Access parameters inside a component using `useParams()`:
::: code-group
```tsx [pages/blog/[slug]/PostHeader.tsx]
import { useParams } from "_/use";
export default function PostHeader() {
const { slug } = useParams();
return
{slug}
;
}
```
```mdx [pages/blog/[slug]/index.mdx]
---
title: Blog Post
---
import PostHeader from "./PostHeader.tsx"
```
:::
`useRoute()` provides the full route context including name, params, and frontmatter:
```tsx
import { useRoute } from "_/use";
export default function Breadcrumb() {
const { name, params, frontmatter } = useRoute();
return ;
}
```
> **Important:** hooks must be called inside a component's render function,
> not at module scope. `export const params = useParams()` in an MDX file
> runs on import and will fail.
## 🔗 Type-Safe Navigation
The generator produces a typed `Link` component at `components/Link.tsx`:
```mdx
import Link from "~/components/Link"
Navigate to the first post
or go home.
```
The `to` prop accepts the same typed tuple as other frameworks - route name
followed by parameters. TypeScript enforces correct parameter types at
compile time.
> **Tip:** When `Link` is enabled in `components/mdx.tsx` (the default),
> it can be used in pages without import - it is a global component provided via `MDXProvider`.
## 📥 Frontmatter & Head Injection
Frontmatter drives `` content automatically. The SSR server reads
`title`, `description`, and the `head` array from frontmatter and injects
them into the HTML template:
```mdx
---
title: Getting Started
description: Set up your first MDX source folder.
head:
- - meta
- name: keywords
content: mdx, kosmojs, getting started
- - link
- rel: canonical
href: https://kosmojs.dev/docs/getting-started
---
```
Produces:
```html
Getting Started
```
This follows the same convention used by VitePress - no new syntax to learn.
## 🏗️ Application Structure
The MDX generator produces the same foundational files as other frameworks,
maintaining a consistent project structure:
```txt
src/content/
├── App.mdx ← global layout
├── router.tsx ← Preact router using createRouter
├── index.html ← HTML shell with placeholders
├── components/
│ ├── Link.tsx ← typed navigation component
│ └── mdx.tsx ← MDXProvider component overrides
├── entry/
│ ├── client.tsx ← minimal client entry (no hydration)
│ └── server.ts ← SSR rendering with Preact
└── pages/
└── *.mdx ← content pages
```
### Router Configuration
The MDX router uses `createRouter` to resolve routes at render time.
```tsx [router.tsx]
import { createRouter } from "_/mdx";
import routerFactory from "_/router";
import App from "./App.mdx";
import { components } from "./components/mdx"
export default routerFactory((routes) => {
const router = createRouter(routes, App, { components });
return {
async clientRouter() {
return router.resolve();
},
async serverRouter(url) {
return router.resolve(url);
},
};
});
```
### Client/Server Entry
Both client and server entries follow the same `renderFactory` pattern as React/Solid/Vue.
* Client entry either renders the whole page on dev or hydrates the rendered SSR page.
* Server entry factory returns `renderToString` with `{ head, html }`.
:::code-group
```tsx [entry/client.tsx]
import { hydrate, render } from "preact";
import renderFactory, { createRoutes } from "_/entry/client";
import routerFactory from "../router";
const routes = createRoutes();
const { clientRouter } = routerFactory(routes);
const root = document.getElementById("app");
if (root) {
renderFactory(() => {
return {
async mount() {
const page = await clientRouter();
render(page.component, root);
},
async hydrate() {
const page = await clientRouter();
hydrate(page.component, root);
},
};
});
} else {
console.error("❌ Root element not found!");
}
```
```ts [entry/server.ts]
import { renderToString } from "preact-render-to-string";
import { renderHead } from "_/mdx";
import renderFactory, { createRoutes } from "_/entry/server";
import routerFactory from "../router";
const routes = createRoutes();
const { serverRouter } = routerFactory(routes);
export default renderFactory(() => {
return {
async renderToString(url, { assets }) {
const page = await serverRouter(url);
const head = assets.reduce(
(head, { tag }) => `${head}\n${tag}`,
renderHead(page?.frontmatter),
);
const html = page ? renderToString(page.component) : "";
return { html, head };
},
};
});
```
:::
## 📦 Static Site Generation
MDX source folders support SSG for deploying to CDNs without a running server.
The build process renders every route to static HTML files.
For routes with dynamic parameters, use `staticParams` to declare the variants:
```mdx [pages/docs/[slug]/index.mdx]
---
title: Documentation
staticParams:
- [getting-started]
- [routing]
- [validation]
---
# {useParams().slug}
```
The build generates a separate HTML file for each entry:
```txt
dist/ssg/
├── index.html
├── docs/
│ ├── getting-started/index.html
│ ├── routing/index.html
│ └── validation/index.html
└── assets/
├── index-abc123.js
└── index-def456.css
```
Static routes (no parameters) render automatically with no additional configuration.
> **Important:** Dynamic routes without `staticParams` are skipped from SSG build,
> that's it, no static files generated for dynamic routes without `staticParams`.
## 💡 When to Use MDX vs Frameworks
| Use Case | MDX | React / SolidJS / Vue |
|---|---|---|
| Documentation sites | ✅ | ❌ Overkill |
| Marketing / landing pages | ✅ | ❌ Overkill |
| Blog with static content | ✅ | ❌ Overkill |
| Interactive dashboards | ❌ | ✅ |
| Apps with client-side state | ❌ | ✅ |
| Forms with real-time validation | ❌ | ✅ |
The rule is simple: if the source folder is primarily content with occasional
interactive components, use MDX. If it is primarily interactive
with occasional content, use React/Vue/Solid.
## ⚠️ Common Pitfalls
* **No TypeScript in MDX.** Keep typed code in `.tsx` files and import into MDX. MDX only supports plain JavaScript expressions.
* **Hooks at module scope.** `export const x = useHook()` runs on import, not during render. Always call hooks inside component functions.
* **Curly braces in prose.** `{...spread}` in markdown text is parsed as a JSX expression. Use backticks for code containing curly braces: `` `{...spread}` ``.
* **Layouts must be `.mdx`.** Plain `.md` files cannot render `{props.children}` and will not work as layouts.
---
---
url: /backend/middleware.md
description: >-
Understand middleware chains and Koa/Hono onion model execution pattern.
Configure middleware to run only for specific HTTP methods. Override global
middleware using slot system.
---
Beyond the standard HTTP method handlers, you often need to run custom middleware -
code that executes before your main handler to perform tasks like authentication,
logging, or data transformation.
## 🔧 Basic Usage
KosmoJS provides the `use` function for applying middleware,
with the same API for both `Koa` and `Hono` routes.
By default, middleware is applied to all HTTP methods:
```ts [api/example/index.ts]
export default defineRoute<"example">(({ GET, POST, use }) => [
use(async (ctx, next) => {
// runs for both GET and POST
return next();
}),
GET(async (ctx) => { /* ... */ }),
POST(async (ctx) => { /* ... */ }),
]);
```
Middleware must call `next()` to pass control to the next layer.
Skipping `next()` short-circuits the chain - useful for early rejections.
## 🔄 Execution Order (Onion Model)
Middleware runs in definition order going in, then unwinds in reverse after the handler.
Consider this example:
```ts [api/example/index.ts]
export default defineRoute<"example">(({ POST, use }) => [
use(async (ctx, next) => {
console.log("First middleware");
await next();
console.log("First middleware after next");
}),
use(async (ctx, next) => {
console.log("Second middleware");
await next();
console.log("Second middleware after next");
}),
POST(async (ctx) => {
console.log("POST handler");
ctx.body = { success: true }; // for Koa
// ctx.json({ success: true }); // for Hono
}),
]);
```
When a POST request arrives, the execution order is like:
```
First middleware
Second middleware
POST handler
Second middleware after next
First middleware after next
```
Global middleware from `api/use.ts` runs first, then route-level `use` calls, then the handler.
**Positioning note:** All `use` calls run before method handlers regardless of where they appear
in the array. Defining `use` after a handler doesn't change this:
```ts
export default defineRoute(({ use, GET, POST }) => [
use(firstMiddleware),
GET(async (ctx) => { /* ... */ }),
POST(async (ctx) => { /* ... */ }),
use(secondMiddleware), // still runs BEFORE handlers [!code hl]
]);
```
## 🎯 Method-Specific Middleware
Use the `on` option to restrict middleware to specific HTTP methods:
```ts [api/example/index.ts]
export default defineRoute<"example">(({ GET, POST, PUT, DELETE, use }) => [
use(async (ctx, next) => {
ctx.state.user = await verifyToken(ctx.headers.authorization);
return next();
}, {
on: ["POST", "PUT", "DELETE"], // [!code hl]
}),
GET(async (ctx) => {
// no auth required
}),
POST(async (ctx) => {
// ctx.state.user is available
}),
]);
```
## 🎛️ Slot Composition
Slots are named positions in the middleware chain. Middleware with the same slot name
replaces earlier middleware at that position - useful for overriding global defaults per-route.
A global error handler defined in `api/use.ts`:
```ts [api/use.ts]
export default [
use(
async (ctx, next) => { /* global logger */ },
{ slot: "logger" },
),
];
```
Override it for a specific route:
```ts [api/upload/index.ts]
export default defineRoute<"upload">(({ POST, use }) => [
use(
async (ctx, next) => {
// custom logger for this route only
},
{ slot: "logger" },
),
POST(async (ctx) => { /* ... */ }),
]);
```
**Important:** When overriding via slot, explicitly set `on` if needed -
it doesn't inherit from the middleware being replaced.
Custom slot names, like `logger`, should be added to `api/env.d.ts`:
```ts [api/env.d.ts]
export declare module "@kosmojs/core/api" {
interface UseSlots {
logger: string; // [!code hl]
}
}
```
Then use it anywhere:
```ts
use(async (ctx, next) => { /* ... */ }, { slot: "logger" })
```
---
---
url: /frontend/layouts.md
description: >-
Compose shared UI at any level of the route hierarchy using layout files.
Navigation, sidebars, auth shells, and data loading scoped to route subtrees
for React, SolidJS, Vue and MDX applications.
---
Layout files wrap groups of routes with shared UI - without duplicating
components across every page.
## 🎨 Define a Layout
Create a `layout.tsx` (or `.vue` / `.mdx`) in any folder under `pages/`,
and it automatically wraps every route in that folder and its subfolders.
Nest layouts by nesting folders.
```
pages/
dashboard/
layout.tsx ← wraps all /dashboard/* pages
settings/
layout.tsx ← wraps all /dashboard/settings/* pages
profile/
index.tsx ← wrapped by both layouts
index.tsx
index.tsx
```
For `/dashboard/settings/profile`, the render order is:
```
App.tsx (global wrapper)
└── dashboard/layout.tsx
└── dashboard/settings/layout.tsx
└── dashboard/settings/profile/index.tsx
```
No configuration, no imports - the file system defines the hierarchy.
Child routes cannot escape parent layouts. Once a layout is established at a
folder level, all routes beneath it inherit it - keeping the UI hierarchy
predictable.
## 📁 Layout File Naming
Only the lowercase form is recognized as a special file. `Layout.tsx`,
`LAYOUT.vue`, and other variations are treated as regular components.
| Framework | Recognized name |
|-----------|----------------|
| React / SolidJS | `layout.tsx` |
| Vue | `layout.vue` |
| MDX | `layout.mdx` |
Each source folder runs a single framework and ignores files belonging to
others: React/SolidJS folders ignore `.vue` files, Vue folders ignore `.tsx`.
When you create a new layout file, `KosmoJS` generates framework-appropriate
boilerplate immediately. Some editors may require a brief unfocus/refocus to
load the generated content.
## 🛠 Layout Implementation
Each framework renders child routes differently:
::: code-group
```tsx [React · layout.tsx]
import { Outlet } from "react-router";
export default function Layout() {
return (