--- 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 (

Custom Template

Route params: {JSON.stringify(params)}

); } `; ``` ```ts [SolidJS] const customTemplate = ` import { useParams } from "@solidjs/router"; export default function Page() { const params = useParams(); return (

Custom Template

Route params: {JSON.stringify(params)}

); } `; ``` ```ts [Vue] const customTemplate = ` `; ``` ```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}
Built with KosmoJS
``` 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 (
...
); } ``` ```tsx [SolidJS · layout.tsx] import type { ParentComponent } from "solid-js"; const Layout: ParentComponent = (props) => { return (
{props.children}
...
); }; export default Layout; ``` ```vue [Vue · layout.vue] ``` ```mdx [MDX · layout.mdx]
{props.children}
Built with KosmoJS
``` ::: React renders child routes via ``. SolidJS and MDX use `props.children`. Vue uses ``. ## 📥 Data Loading in Layouts Layout data loading follows the same per-framework patterns as page components: ::: code-group ```tsx [React · layout.tsx] import { Outlet, useLoaderData } from "react-router"; import fetchClients, { type ResponseT } from "_/fetch"; export const loader = fetchClients["dashboard/data"].GET; export default function Layout() { const data = useLoaderData(); // ... return ; } ``` ```tsx [SolidJS · layout.tsx] import type { ParentComponent } from "solid-js"; import { createAsync } from "@solidjs/router"; import fetchClients from "_/fetch"; export const preload = fetchClients["dashboard/data"].GET; const Layout: ParentComponent = (props) => { const data = createAsync(preload); // ... return <>{props.children}; }; export default Layout; ``` ```vue [Vue · layout.vue] ``` ::: React and SolidJS loaders/preloads run before the layout renders - data is available immediately and shared across all child routes without duplicate fetches. Vue requires manual lifecycle management via `onMounted` or navigation guards; see the [Vue Router documentation](https://router.vuejs.org/guide/advanced/data-fetching.html) for advanced patterns. ## 🌐 Global Layout via App File The `App.{tsx,vue,mdx}` at the source folder root wraps every route - the right place for truly global concerns like authentication checks, analytics tracking or error boundaries. ```txt front/ ├── App.tsx ← wraps everything └── pages/ ├── dashboard/ │ └── layout.tsx └── index/ └── index.tsx ``` ## 📊 Layout Hierarchy Example For a deeply nested route like `/dashboard/settings/security`: ```txt front/ ├── App.tsx ← Level 1: global wrapper └── pages/ └── dashboard/ ├── layout.tsx ← Level 2: dashboard wrapper └── settings/ ├── layout.tsx ← Level 3: settings wrapper └── security/ ├── layout.tsx ← Level 4: security wrapper └── index.tsx ← Level 5: page component ``` Renders as: ``` App └── Dashboard Layout └── Settings Layout └── Security Layout └── Security Page ``` ## 💡 Best Practices * **Keep layouts focused.** Each layout handles concerns for its own scope - dashboard navigation in the dashboard layout, not global auth state. * **Fetch shared data at the right level.** If multiple child routes need the same data, load it in their common parent layout rather than duplicating the fetch. * **Use layouts for shared behavior.** Beyond UI structure, layouts suit shared logic: permission checks, analytics, or subscription state scoped to a route group. * **Avoid deep nesting without purpose.** Three or four levels is reasonable. Beyond that, consider whether the hierarchy reflects genuine UI structure or accidental complexity. * **Handle loading states explicitly.** Layout data loading can delay rendering - show appropriate fallbacks, especially in Vue where loading is managed manually. ## ⚠️ Common Pitfalls * **Case sensitivity.** Only `layout.{tsx,vue,mdx}` are recognized as layout files. * **Framework file isolation.** `.vue` files in a React/SolidJS/MDX folder are ignored, and `.tsx/.mdx` files in a Vue folder are ignored. * **No layout opt-out.** Child routes always inherit parent layouts. Routes that shouldn't share a layout belong in a different directory branch. * **Data loading differs by framework.** React and SolidJS have built-in route-level data patterns. Vue requires manual lifecycle management. MDX layouts delegate data loading to a separate component in a `.tsx` file. --- --- url: /openapi.md description: >- Automatically generate OpenAPI 3.1 specifications from KosmoJS API routes. Analyzes route structure, TypeScript types, and validation schemas to produce standards-compliant documentation. --- `KosmoJS` generates an `OpenAPI 3.1` specification directly from your route definitions. Route structure, `TypeScript` types, `VRefine` constraints, parameters, responses - all reflected in the spec automatically. No manual schema authoring, no annotation layers. ## 🔧 Enable the generator Simply add it to your source folder's `kosmo.config.ts`: ```ts import { defineConfig, // ...other generators openapiGenerator, // [!code ++] } from "@kosmojs/dev"; const openapiConfig = { // [!code ++:3] // ... }; export default defineConfig({ generators: [ // ...other generators openapiGenerator(openapiConfig), // [!code ++] ], }); ``` ## ⚙️ Configuration ### Required Options **`outfile`** - Path where the spec is written, relative to your `kosmo.config.ts`. **`openapi`** - OpenAPI version. Use `"3.1.0"` or any `3.1.x` version. **`info`** - API metadata: * `title` (required) - Name of your API * `version` (required) - API version, use semantic versioning **`servers`** - Array of server objects: * `url` (required) - Base URL where the API is served * `description` (optional) - Human-readable label ### Optional Info Properties **`summary`** - One-line summary **`description`** - Detailed description, supports markdown **`termsOfService`** - URL to terms of service **`contact`** - `name`, `url`, `email` **`license`** - `name` (required), `identifier` (SPDX), `url` ### Complete Example ```typescript const openapiConfig = { outfile: "openapi.json", openapi: "3.1.0", info: { title: "My SaaS API", version: "2.1.0", summary: "RESTful API for My SaaS Platform", description: ` # API Documentation This API provides access to all platform features including user management, billing, and analytics.`, termsOfService: "https://myapp.com/terms", contact: { name: "API Support", url: "https://myapp.com/support", email: "api@myapp.com", }, license: { name: "Apache 2.0", url: "https://www.apache.org/licenses/LICENSE-2.0.html", }, }, servers: [ { url: "http://localhost:4556", description: "Development server" }, { url: "https://staging-api.myapp.com", description: "Staging environment" }, { url: "https://api.myapp.com", description: "Production server" }, ], }; ``` ## 📄 Generated Specification The output is a complete `OpenAPI 3.1` document covering: * **Paths** - all routes with HTTP methods, parameters, request bodies, and responses * **Schemas** - type definitions extracted from your `TypeScript` types and validation schemas * **Parameters** - path, query, and header parameters with types and constraints * **Request Bodies** - payload schemas for POST, PUT, and PATCH endpoints * **Responses** - response schemas with status codes and content types * **Validation Rules** - `VRefine` constraints appear as JSON Schema keywords ### Path Variations for Optional Parameters OpenAPI requires all path parameters to be mandatory, so routes with optional parameters generate multiple paths. For a route at `users/[id]/posts/{postId}/index.ts`, the generator produces: * `/users/{id}/posts/{postId}` - full path with optional parameter present * `/users/{id}/posts` - path without optional parameter Both reference the same handlers and schemas. ### Live Regeneration The spec regenerates automatically whenever you modify route definitions, types, or validation schemas. The generator runs in the background alongside the validation and fetch generators - no manual rebuild step required. Serve the generated spec with any standard tooling: [Swagger UI](https://swagger.io/tools/swagger-ui/), [Redoc](https://github.com/Redocly/redoc), or [Stoplight Elements](https://stoplight.io/open-source/elements). --- --- url: /validation/params.md description: >- Validate route parameters at runtime with type refinements using defineRoute type arguments. Convert string URL parameters to validated numbers, integers, or constrained values with VRefine. --- Route parameters are extracted from the URL path as strings - that's just how URLs work. But you often need more than a string: a numeric ID, a positive integer, a value from a fixed set or of a specific pattern, uuid, date, etc. `KosmoJS` lets you express these requirements directly in the type system. ## 🎯 Params Refinements Pass a tuple as the second type argument to `defineRoute`. Each position refines the corresponding parameter in route order. For a route at `api/users/[id]/index.ts`: ```ts [api/users/[id]/index.ts] import { defineRoute } from "_/api"; export default defineRoute<"users/[id]", [ number // [!code hl] ]>(({ GET }) => [ GET(async (ctx) => { const { id } = ctx.validated.params; // typed as number // [!code hl] }), ]); ``` A request to `/api/users/abc` is rejected with a 400 before your handler runs. Access validated parameters through `ctx.validated.params` - it carries the refined type, not the raw string. The underlying `ctx.params` (Koa) or `ctx.req.param()` (Hono) still exists if you need the original. Refine further with `VRefine` (globally available, no import needed): ```ts [api/users/[id]/index.ts] export default defineRoute<"users/[id]", [ VRefine // positive integer // [!code hl] ]>(({ GET }) => [ // ... ]); ``` [More on VRefine ➜ ](/validation/refine) ## 🚥 Multiple Parameters For routes with multiple parameters, each tuple position maps to the corresponding param. Positions are optional - omit any you don't need to refine. ```ts [api/users/[id]/[view]/index.ts] export default defineRoute<"users/[id]/[view]", [ VRefine, // id // [!code hl] "profile" | "settings" | "data" // view // [!code hl] ]>(({ GET }) => [ GET(async (ctx) => { const { id, view } = ctx.validated.params; // [!code hl] }), ]); ``` Refinements are positional, not name-based - renaming `[id]` to `[userId]` requires no changes here. --- --- url: /fetch/utilities.md description: >- Build URLs with path and href utility functions that handle route parameters, query strings, and base URL configuration for navigation and external references. --- Each fetch client exposes `path` and `href` for building URLs without making a request - useful for navigation, `` tags, or passing URLs to other services. ```ts [pages/example/index.tsx] import fetchClients from "_/fetch"; const useFetch = fetchClients["users/[id]"]; useFetch.path([123]); // → "/api/users/123" useFetch.path([123], { query: { include: "posts" } }); // → "/api/users/123?include=posts" useFetch.href("https://api.example.com", [123]); // → "https://api.example.com/api/users/123" useFetch.href("https://api.example.com", [123], { query: { include: "posts" } }); // → "https://api.example.com/api/users/123?include=posts" ``` Multiple parameters follow path order: ```ts // route: posts/[userId]/comments/[commentId] useFetch.path([456, 789]); // → "/api/posts/456/comments/789" ``` --- --- url: /validation/payload.md description: >- Validate request payloads with inline TypeScript types or imported type definitions. Support for nested structures, conditional validation, generics, and complex domain models with VRefine constraints. --- Payload validation covers everything your API receives from clients: query parameters, headers, cookies for any method, and request bodies for POST/PUT/PATCH. `KosmoJS` makes payload validation straightforward by letting you express validation rules directly through `TypeScript` types. You write the type once, and it serves both as compile-time safety and runtime validation enforcement. ## 🎯 Validation Targets Each target maps to a part of the incoming HTTP request. **Metadata targets** (all HTTP methods): * `query` - URL query parameters (`?page=1&limit=10`) * `headers` - HTTP request headers * `cookies` - HTTP cookies **Body targets** (POST/PUT/PATCH only): * `json` - JSON request body * `form` - URL-encoded or multipart form * `raw` - plain text, binary, Buffer, ArrayBuffer, Blob Any combination of metadata targets is valid. Body targets are mutually exclusive - one per handler. ```ts // ✅ Multiple metadata targets + one body target POST<{ query: { page: number }; headers: { authorization: string }; json: { title: string }; }> // ✅ Only metadata targets GET<{ query: { search: string }; headers: { 'x-api-key': string }; cookies: { session: string }; }> // ❌ Multiple body targets POST<{ json: { title: string }; form: { title: string }; // Error: only one body target allowed }> // ❌ Body target on GET GET<{ json: { data: string }; // Error: GET cannot have request body }> ``` Invalid configurations are detected at dev time - `KosmoJS` warns and disables affected schemas automatically. ## 📦 Basic Payload Validation Pass the payload type as the first type argument to your method handler: ```ts [api/posts/index.ts] import { defineRoute } from "_/api"; export default defineRoute<"posts">(({ POST }) => [ POST<{ json: { title: VRefine; content: string; tags: string[]; isPublished: boolean; scheduledPublishAt?: VRefine; }, }>(async (ctx) => { const { title, content, tags } = ctx.validated.json; // [!code hl] }), ]); ``` Optional fields (`?`) can be omitted entirely. When present, they must still pass their type and refinement constraints. Notice how `VRefine` is used to add validation constraints to specific fields. The `minLength` and `maxLength` constraints ensure titles aren't empty or excessively long. The `format: "date-time"` constraint leverages JSON Schema's format validation to ensure dates are properly formatted. Without `VRefine`, fields are validated only for their basic type - a string must be a string, an array must be an array, but no additional constraints apply. [More on VRefine ➜ ](/validation/refine) ## 🏗️ Complex Nested Structures Since validation rules are just `TypeScript` types, nested objects, union types, conditional fields, referenced types - all work naturally: ```ts [api/example/index.ts] import type { BillingAddress } from "~/types/checkout"; type PaymentMethod = { type: "card" | "wallet"; card?: { number: VRefine; expMonth: VRefine; expYear: number; cvc: VRefine; holderName: string; }; wallet?: { walletId: string; token: VRefine; }; }; type Payload = { orderId: VRefine; amount: VRefine; currency: VRefine; paymentMethod: PaymentMethod; billingAddress: BillingAddress; }; export default defineRoute(({ POST }) => [ POST<{ json: Payload, }>(async (ctx) => { // every field validated before handler runs }), ]); ``` ## 🎨 Combining Multiple Targets You can validate multiple parts of the request simultaneously by specifying multiple targets. This is particularly useful for endpoints that need to validate query parameters, headers, and request body together: ```ts [api/posts/search.ts] export default defineRoute<"posts/search">(({ POST }) => [ POST<{ query: { page: VRefine; limit: VRefine; sortBy?: "date" | "title" | "views"; }; headers: { authorization: VRefine; "x-api-version"?: string; }; cookies: { session: string; }; json: { filters: { tags?: string[]; status?: "draft" | "published" | "archived"; dateRange?: { from: VRefine; to: VRefine; }; }; }; }>(async (ctx) => { const { page, limit, sortBy } = ctx.validated.query; const { authorization } = ctx.validated.headers; const { session } = ctx.validated.cookies; const { filters } = ctx.validated.json; }), ]); ``` Each target is validated independently. All must pass before your handler executes. ## 📝 Body Formats ### JSON ```ts POST<{ json: { name: string; email: VRefine; }; }>(async (ctx) => { const { name, email } = ctx.validated.json; }) ``` ### Form (URL-encoded) ```ts POST<{ form: { username: VRefine; password: VRefine; }; }>(async (ctx) => { const { username, password } = ctx.validated.form; }) ``` ### Multipart (file uploads) ```ts POST<{ form: { file: File; title: string; description?: string; }; }>(async (ctx) => { const { file, title, description } = ctx.validated.form; }) ``` ### Raw ```ts POST<{ raw: VRefine; }>(async (ctx) => { const rawContent = ctx.validated.raw; }) ``` **Worth noting:** you can only specify **one body target** per handler (`json`, `form` or `raw`), but you can combine it with any number of metadata targets (`query`, `headers`, `cookies`). ## 🔗 Referenced Types As your application grows, defining complex types inline becomes unwieldy. You'll want to define types once and reuse them across multiple routes. `KosmoJS` fully supports this pattern - you can define types in separate files, import them where needed, and use them for validation just like inline types. Suppose you have a file defining user-related types: ```ts [types/user.ts] export type UserProfile = { name: VRefine; email: VRefine; }; export type UserPreferences = { theme: "light" | "dark"; notifications: NotificationPreferences; }; type NotificationPreferences = { enabled: boolean; }; ``` ```ts [types/api-payload.ts] import type { UserProfile, UserPreferences } from "./user"; export type Payload = { data: T; meta: { pagination?: { page: number; limit: number; total: number }; cache: { ttl: number; revalidate: boolean }; }; }; export type User = { id: number; profile: UserProfile; preferences: UserPreferences; posts: Post[]; }; export type Post = { id: VRefine; title: string; tags: { id: string; name: string }[]; stats?: { views: number; likes: number }; }; ``` Now you can use these types in any route by importing them: ```ts [api/users/index.ts] import type { User, Payload } from "~/types/api-payload"; import { defineRoute } from "_/api"; export default defineRoute<"users">(({ POST }) => [ POST<{ json: Payload, // [!code hl] }>(async (ctx) => { // ctx.validated.json typed as Payload }), ]); ``` The generator resolves generics, traces all referenced types, and builds a complete validation schema. Update a shared type and validation updates everywhere it's used. Different routes, same wrapper: ```ts [api/posts/index.ts] import type { Post, Payload } from "~/types/api-payload"; export default defineRoute<"posts">(({ POST }) => [ POST<{ json: Payload, // [!code hl] }>(async (ctx) => {}), ]); ``` --- --- url: /start.md description: Get a KosmoJS project running in under five minutes. --- Zero to a working route in under five minutes. [StackBlitz Playground](https://stackblitz.com/edit/stackblitz-starters-jlxz7lzc?file=README.md) [![asciicast](https://asciinema.org/a/968086.svg)](https://asciinema.org/a/968086) ## 🚀 Create & install ::: code-group ```sh [pnpm] pnpm create kosmo cd my-app pnpm install ``` ```sh [npm] npm create kosmo cd my-app npm install ``` ```sh [yarn] yarn create kosmo cd my-app yarn install ``` ::: ## 📁 Add a source folder ::: code-group ```sh [pnpm] pnpm +folder pnpm install ``` ```sh [npm] npm run +folder npm install ``` ```sh [yarn] yarn +folder yarn install ``` ::: You'll be prompted for a folder name, base URL, framework, and backend. The second install pulls in framework-specific dependencies. ## ✅ Create a route Create the file `api/users/[id]/index.ts` - `KosmoJS` detects it and generates starter code automatically. Replace the generated content with something real: ::: code-group ```ts [Koa] import { defineRoute } from "_/api"; type User = { id: number; name: string; email: string } export default defineRoute<"users/[id]">(({ GET }) => [ GET(async (ctx) => { const { id } = ctx.params; ctx.body = { id: Number(id), name: "Jane Smith", email: "jane@example.com" }; }), ]); ``` ```ts [Hono] import { defineRoute } from "_/api"; type User = { id: number; name: string; email: string } export default defineRoute<"users/[id]">(({ GET }) => [ GET(async (ctx) => { const { id } = ctx.req.param(); ctx.json({ id: Number(id), name: "Jane Smith", email: "jane@example.com" }); }), ]); ``` ::: ## ⚡ Start the dev server ::: code-group ```sh [pnpm] pnpm dev ``` ```sh [npm] npm run dev ``` ```sh [yarn] yarn dev ``` ::: Visit `http://localhost:4556/api/users/123`. You should see JSON. ## 🎨 Create a page With the dev server still running, create `pages/users/[id]/index.tsx` (or `.vue`). `KosmoJS` generates a placeholder component - replace it with a page that fetches from your API route: ::: code-group ```tsx [React] import { useState, useEffect } from "react"; import { useParams } from "react-router"; import fetchClients from "_/fetch"; const { GET } = fetchClients["users/[id]"]; export default function UserPage() { const { id } = useParams(); const [user, setUser] = useState(null); useEffect(() => { GET([id]).then(setUser); }, [id]); return user ?

{user.name}

{user.email}

:
Loading...
; } ``` ```tsx [SolidJS] import { useParams } from "@solidjs/router"; import { createAsync } from "@solidjs/router"; import fetchClients from "_/fetch"; const { GET } = fetchClients["users/[id]"]; export default function UserPage() { const params = useParams(); const user = createAsync(() => GET([params.id])); return user() ?

{user().name}

{user().email}

:
Loading...
; } ``` ```vue [Vue] ``` ::: Visit `http://localhost:4556/users/123`. Your page renders with data from the API. The fetch client is fully typed - `user.name` and `user.email` autocomplete in your editor, and invalid parameters are caught before the request leaves the browser. ## ✨ What just happened Your folder structure became your routes: ``` api/users/[id]/index.ts ➜ /api/users/:id pages/users/[id]/index.tsx ➜ /users/:id ``` `[id]` is a required parameter. `{id}` makes it optional. `{...path}` matches any depth. The parallel structure between `api/` and `pages/` is intentional - API endpoints and their corresponding pages are always easy to find. The fetch client was generated automatically from your API route definition. Change the API types, and the client updates with them - no manual sync. *** That's the foundation. From here: * [Tutorial](/tutorial) - validation, middleware, fetch clients, pages, SSR * [Routing](/routing/intro) - parameters, mixed segments, power syntax * [Features](/features) - everything KosmoJS provides, at a glance --- --- url: /validation/response.md description: >- Validate API responses before sending to clients. Catch bugs where handlers return incomplete objects, wrong types, or unexpected structures with automatic runtime checking. --- Outgoing responses can be validated too. Use the `response` property to declare the expected status code, content type, and body schema: ```ts [api/users/index.ts] import type { User } from "~/types/api-payload"; import { defineRoute } from "_/api"; export default defineRoute<"users">(({ GET }) => [ GET<{ response: [200, "json", User], // [!code hl] }>(async (ctx) => { // response must comply with the defined schema }), ]); ``` Before sending, `KosmoJS` checks that the actual status, content type, and body match the schema. If anything is off - a missing field, a type mismatch, a constraint violation - it throws a `ValidationError` instead of sending malformed data to the client. Response validation is especially valuable for data sourced from databases or third-party APIs, where the shape can change without warning. Defining a response schema also enables automatic `OpenAPI` generation - type safety and documentation in one step. ([Details ➜ ](/openapi)) --- --- url: /frontend/routing.md description: >- Watch-based route generation, lazy-loaded components, data loading integration, and nested layout patterns for React, SolidJS, Vue and MDX applications. --- Each framework generator continuously watches your `pages` directory. When a page component is created, the generator analyzes its filesystem location, produces a corresponding route configuration, and writes it to your `lib` directory for the router to consume - without any manual wiring. ## 🛣️ Same routing, both sides Frontend routing follows the exact same directory-based pattern as API routing. If you know how `api/` routes work, you already know how `pages/` routes work: ``` api/users/[id]/index.ts ➜ /api/users/:id (backend handler) pages/users/[id]/index.tsx ➜ /users/:id (frontend component) ``` The parallel structure is intentional - an API endpoint and its corresponding page are always one folder apart. The same parameter syntax applies to both: | Syntax | Type | Example | |---|---|---| | `[id]` | Required | `pages/users/[id]/` ➜ `/users/123` | | `{id}` | Optional | `pages/users/{id}/` ➜ `/users` or `/users/123` | | `{...path}` | Splat | `pages/docs/{...path}/` ➜ `/docs/any/depth` | Static routes always take priority over dynamic ones. Optional parameters followed by static segments can cause ambiguity - see [parameter details](/routing/params) for gotchas and solutions. ## 🪆 Layouts Layout files wrap groups of pages with shared UI - navigation, sidebars, auth shells - at any level of the route hierarchy: ``` pages/ dashboard/ layout.tsx - wraps all /dashboard/* pages settings/ layout.tsx - wraps all /dashboard/settings/* pages index.tsx index.tsx ``` Layouts stack outward-in and cannot be escaped by child routes. [More on Layouts ➜](/frontend/layouts) ## 📦 Lazy Loading All page components are lazy-loaded by default. Route code is excluded from the initial JavaScript bundle and fetched on demand when a user navigates to that path. This keeps initial payloads small, accelerates application startup, and ensures users download only the code for routes they actually visit. ## 🗺️ Generated Route Shape The route object written to `lib` differs slightly per framework to match each router's expected format: ::: code-group ```ts [React] // React Router receives a flat route definition. // The loader is wired automatically when your page exports one. { path: "/users/:id", lazy: () => import("@/src/front/pages/users/[id]/index.tsx"), } ``` ```ts [SolidJS] // SolidJS Router receives component and preload as separate keys. // The preload function is called on link hover and navigation intent. { path: "/users/[id]", component: lazy(() => import("@/src/front/pages/users/[id]/index.tsx")), preload: () => import("@/src/front/pages/users/[id]/index.tsx").then( (mdl) => (mdl as ComponentModule).preload?.() ), } ``` ```ts [Vue] // Vue Router receives a standard lazy route definition. { name: "users/[id]", path: "/users/[id]", component: () => import("@/src/front/pages/users/[id]/index.vue"), } ``` ::: ## 🔄 Data Loading on Navigation React and SolidJS integrate data fetching directly into the route lifecycle. **React** - when a page exports a `loader` function, React Router executes it at strategic moments: initial page load, link hover, and navigation initiation. Data is available before the component renders, eliminating loading spinners for route-level data. **SolidJS** - when a page exports a `preload` function, SolidJS Router calls it on link hover and navigation intent. The preload result is cached and reused by `createAsync` inside the component, so no duplicate requests are made. **Vue** - preload hooks are not yet part of the Vue generator. Route-level data fetching is handled through navigation guards in `router.ts` or reactive `setup()` logic within the component. Prefetching support is under consideration - contributions are welcome! 🙌 --- --- url: /validation/intro.md description: >- KosmoJS runtype validation automatically converts TypeScript types into JSON Schema with runtime validators. Write types once, get compile-time and runtime safety without schema duplication. --- `KosmoJS` uses the "runtype" validation approach - your `TypeScript` types are automatically converted into JSON Schema and validated at runtime. No separate schema language to learn. No schemas drifting out of sync with your types. One type definition becomes the source of truth for: * runtime validation on the server * client-side validation in generated fetch clients * OpenAPI 3.1 specification ## 🛡️ Understanding Runtype Validation When you provide type annotations to your route parameters, payloads, and responses, `TypeScript` gives you compile-time checking - autocomplete, refactoring safety, and error detection before you run your code. But compile-time checks don't protect you at runtime. When actual HTTP requests arrive with unpredictable data from the outside world, `TypeScript` is no longer in the picture. Runtype validation closes this gap. The same type definitions that give you compile-time safety also generate validation logic that runs when requests arrive, ensuring that incoming data actually matches what your types promise. ## 🔄 End-to-End Validation Runtype validation happens at both ends: the client validates before sending requests, and the server validates before processing them. Generated fetch clients validate request data on the client side before making any network request. If validation fails, the client throws immediately - no round trip needed. This uses the exact same schemas that validate on the server, so what the client considers valid and what the server accepts are always in sync. Double validation is not a performance cost - it's a performance gain. Invalid requests never reach your server, saving bandwidth and compute. Users also get instant feedback instead of waiting for a server response. ## 🔍 How Generation Works `KosmoJS` uses AST parsing to extract types from your route files, then AOT compilation to generate high-performance validation routines in your `lib` directory. For each validated type, [TypeBox](https://github.com/sinclairzx81/typebox) produces a JSON Schema and a compiled validator function tailored specifically to that structure - direct property checks with minimal overhead, not a generic JSON Schema interpreter. The generated code lives in `lib`, keeping your source directories focused on business logic. At production build time it's bundled like any other dependency. You don't need to read or understand the generated code to use validation. If you're curious about performance characteristics or need to troubleshoot, see [Validation Performance](/validation/performance). --- --- url: /frontend/server-side-render.md description: >- Add SSR capabilities to React, SolidJS, Vue and MDX applications using the KosmoJS SSR generator. String and stream rendering patterns, production builds, and deployment configurations for server-rendered applications. --- Source folders default to client-side rendering with Vite's dev server and HMR. The SSR generator adds production-ready server rendering while keeping your development workflow unchanged. ## 🛠️ Adding SSR Support SSR is automatically enabled if selected during source folder creation. To add it to an existing folder, register `ssrGenerator` in your source folder's `kosmo.config.ts`: ```ts [kosmo.config.ts] import { defineConfig, // ...other generators ssrGenerator, // [!code ++] } from "@kosmojs/dev"; export default defineConfig({ generators: [ // ...other generators ssrGenerator(), // [!code ++] ], }); ``` ## 📄 Server Entry Point The SSR generator creates `entry/server.tsx` (or `.vue`) with a default implementation. `renderFactory` accepts a callback returning an object with rendering methods: * `renderToString(url, SSROptions)` - renders the complete page before transmission. Provided by default. * `renderToStream(url, SSROptions)` - optional progressive streaming implementation. When provided, takes precedence over `renderToString`. ::: code-group ```ts [React] import { renderToString } from "react-dom/server"; import renderFactory, { createRoutes } from "_/entry/server"; import routerFactory from "../router"; const routes = createRoutes({ withPreload: false }); const { serverRouter } = routerFactory(routes); export default renderFactory(() => { return { async renderToString(url, { assets }) { const page = await serverRouter(url); const head = assets.map(({ tag }) => tag).join("\n"); const html = renderToString(page); return { head, html }; }, }; }); ``` ```ts [SolidJS] import { renderToString, generateHydrationScript } from "solid-js/web"; import renderFactory, { createRoutes } from "_/entry/server"; import routerFactory from "../router"; const routes = createRoutes({ withPreload: false }); const { serverRouter } = routerFactory(routes); export default renderFactory(() => { const hydrationScript = generateHydrationScript(); return { async renderToString(url, { assets }) { const page = await serverRouter(url); const head = assets.reduce( (head, { tag }) => `${head}\n${tag}`, hydrationScript, ); const html = renderToString(() => page); return { head, html }; }, }; }); ``` ```ts [Vue] import { renderToString } from "vue/server-renderer"; 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.map(({ tag }) => tag).join("\n"); const html = await renderToString(page); return { head, html }; }, }; }); ``` ```tsx [MDX] import { renderToString } from "preact-render-to-string"; import renderFactory, { createRoutes } from "_/entry/server"; import { renderHead } from "_/mdx"; 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 }; }, }; }); ``` ::: `renderToString` receives the URL being requested and must return: * `html` - the rendered application markup * `head` - HTML to inject into `` (optional) SolidJS injects a hydration script in `` via `generateHydrationScript()`, which bootstraps client-side reactivity during hydration. ## 🎛️ Render Factory Arguments `renderToString` receives two arguments - the URL and SSROptions: ```ts export type SSROptions = { // The original client index.html output from Vite build. // Contains and placeholders // where SSR content should be injected. template: string; // Vite's final manifest.json - the full dependency graph for // client modules, dynamic imports, and related CSS. manifest: Manifest; // SSR-related assets, must be injected manually (unlike CSR assets that are injected by Vite). // Each entry provides three ways to consume the asset: // - `tag`: ready-to-use HTML tag ( ``` ::: Server-side validation still runs even when endpoints are called directly - client validation is additive, not a substitute. [Read more ➜](/fetch/intro) ## 🎨 Create Client Pages Pages live in `pages/` and follow the same directory-based routing as API routes. Create `pages/users/index.tsx` - `KosmoJS` generates framework-specific boilerplate. Add a layout for shared UI across route groups - create `pages/users/layout.tsx`: ```txt pages/ └── users/ ├── layout.tsx ← wraps all pages under /users └── index.tsx ``` Layouts can be nested - deeper layouts wrap inner layouts, matching your route hierarchy. [Read more ➜](/frontend/routing) ## ⚡ Server-Side Rendering Enable when creating a source folder (`--ssr`), or add it later in `kosmo.config.ts`: ```ts [kosmo.config.ts] import { defineConfig, ssrGenerator } from "@kosmojs/dev"; // [!code ++] export default defineConfig({ generators: [ // ... ssrGenerator(), // [!code ++] ] }); ``` > Restart dev server after adding new generators. `KosmoJS` generates `entry/server.ts` - your SSR orchestration file. Critical CSS is extracted and inlined automatically; remaining styles load asynchronously. Build and run: ```sh pnpm build node dist/front/ssr/server.js -p 4556 ``` The API server and SSR server are bundled separately - deploy, scale, and run them independently. [Read more ➜](/frontend/server-side-render) ## 🗂️ Multiple Source Folders Add more source folders as your project grows - each with its own framework stack, base URL, and build output. They develop and deploy independently but share types, validation logic, and infrastructure. ```sh pnpm +folder # add a new source folder pnpm dev # runs all source folders in parallel pnpm build admin # build a specific folder ``` *** ### Next Steps **Core patterns:** [Routing](/routing/intro) · [Validation](/validation/intro) · [Middleware](/backend/middleware) · [Layouts](/frontend/routing) · [Fetch Clients](/fetch/start) **Advanced:** [VRefine](/validation/refine) · [OpenAPI](/openapi) · [Production Builds](/backend/building-for-production) --- --- url: /backend/type-safety.md description: >- Refine params/payload/response types in KosmoJS with compile-time TypeScript checking and automatic runtime validation using type arguments --- Type safety in `KosmoJS` covers the full request-response cycle: path parameters, payloads, responses, and context/state properties - all driving both compile-time checking and runtime validation from the same type definitions. ## 🔗 Typing Params Parameters are strings by default. Refine them via the second type argument to `defineRoute` by providing a tuple where each position maps to the corresponding parameter in the path: ```ts [api/users/[id]/{action}/index.ts] type UserAction = "retrieve" | "update" | "delete"; export default defineRoute<"users/[id]/{action}", [ number, // id UserAction, // action ]>(({ GET }) => [ GET(async (ctx) => { const { id, action } = ctx.validated.params; // id: number, action: UserAction | undefined }), ]); ``` Positions are optional - but to refine the second param you must also provide the first. ### ❗ Inline Tuple Requirement The refinement tuple must be declared inline. Individual type aliases are fine, but a pre-defined tuple type won't work: ```ts // ✅ works defineRoute<"[id]/[action]", [UserID, UserAction]> // ❌ won't work - tuple reference loses structural info needed for schema generation type Params = [UserID, UserAction]; defineRoute<"[id]/[action]", Params> ``` Refinements also generate runtime validation - invalid params are rejected before your handler runs. ([Details ➜ ](/validation/params)) ## 🔋 Typing Payload & Response The first type argument to each method handler defines payload and response schemas: ```ts [api/example/index.ts] import type { User } from "~/types"; export default defineRoute<"example">(({ POST }) => [ POST<{ json: { name: string; email: string; status?: string }, response: [200, "json", User], }>(async (ctx) => { const { name, email, status } = ctx.validated.json; const user = await createUser({ name, email, status }); ctx.body = user; // Koa // ctx.json(user); // Hono }), ]); ``` Both payload and response are validated at runtime, not just at compile time. ([Details ➜ ](/validation/payload)) ## 📋 Typing State & Context `defineRoute` accepts four type arguments: ::: code-group ```ts [Koa] defineRoute< RouteName, // required ParamsTuple, // param refinements State, // route-specific state/locals Context, // route-specific context properties > ``` ```ts [Hono] defineRoute< RouteName, // required ParamsTuple, // param refinements Variables, // route-specific locals Bindings, // route-specific bindings > ``` ::: Use the third and fourth arguments for types that are unique to a specific route: ::: code-group ```ts [Koa] export default defineRoute< "users/[id]", [number], { permissions: Array<"read" | "write"> }, // ctx.state.permissions { authorizedUser: User }, // ctx.authorizedUser >(({ GET }) => [ GET(async (ctx) => { const { id } = ctx.validated.params; const { permissions } = ctx.state; const { authorizedUser } = ctx; }), ]); ``` ```ts [Hono] export default defineRoute< "users/[id]", [number], { permissions: Array<"read" | "write"> }, // ctx.get("permissions") { DB: D1Database }, // Cloudflare binding >(({ GET }) => [ GET(async (ctx) => { const { id } = ctx.validated.params; const permissions = ctx.get("permissions"); const db = ctx.env.DB; }), ]); ``` ::: If you find yourself declaring the same properties across many routes, move them to the global declarations in `api/env.d.ts` instead. ## ⚙️ Global Context Types - `api/env.d.ts` `api/env.d.ts` extends the default context and state interfaces globally, so every route handler picks them up automatically: ::: code-group ```ts [Koa: api/env.d.ts] export declare module "_/api" { interface DefaultState { permissions: Array<"read" | "write" | "admin">; } interface DefaultContext { authorizedUser: User; } } ``` ```ts [Hono: api/env.d.ts] export declare module "_/api" { interface DefaultVariables { permissions: Array<"read" | "write" | "admin">; } interface DefaultBindings { DB: D1Database; } } ``` ::: `api/use.ts` defines global middleware that runs for every endpoint - the right place to set these properties so they're always available: ```ts [api/use.ts] import { use } from "_/api"; export default [ use(async (ctx, next) => { ctx.state.permissions = await getPermissions(ctx); // Koa // ctx.set("permissions", await getPermissions(ctx)); // Hono return next(); }), ]; ``` **Important:** declaring types in `env.d.ts` doesn't set the values - you still need the middleware that actually populates them. --- --- url: /frontend/link-navigation.md description: >- Generated Link component wrapping each framework's native router with compile-time route validation. Autocomplete navigation targets, parameter enforcement, and query string handling for React, SolidJS, Vue and MDX. --- The generator produces a `Link` component that wraps each framework's native router link with compile-time route validation. It knows your complete route structure and parameters, delivering autocomplete and type checking throughout navigation code. The component is available at `components/Link.tsx` (or `Link.vue`) in your source folder. ## 🔗 Usage The API is consistent across all frameworks - a `to` prop accepting a typed tuple, an optional `query` prop for search parameters, and standard router props passed through: ::: code-group ```tsx [Menu.tsx] import Link from "~/components/Link"; export default function Menu() { return ( ); } ``` ```vue [Menu.vue] ``` ::: Omitting `to` targets the current location - useful for adding or updating query parameters without triggering navigation: ::: code-group ```tsx [Menu.tsx] Filter Active Items ``` ```vue [Menu.vue] Filter Active Items ``` ::: ## 🏷️ LinkProps Type The `to` prop is typed as `LinkProps` - a discriminated union generated from your route structure: ```ts export type LinkProps = | ["index"] | ["users/[id]", id: string | number] | ["posts/[slug]", slug: string] // ... all other routes ``` Typing the first array element triggers TypeScript's IntelliSense with valid route suggestions. Selecting a parameterized route requires providing those parameters as subsequent array elements - the type system enforces this. Renaming a route directory produces TypeScript errors at every `Link` referencing the old name, turning refactors into an automated checklist. --- --- url: /.vitepress/theme/components/ParamsValidation.md --- ```ts [api/users/[id]/{activity}/index.ts] import { defineRoute } from "_/api"; export default defineRoute< "users/[id]/{activity}", [ // validate id param as number // [!code hl] number, // activity, if given, should be one of // [!code hl] "posts" | "comments" | "likes", ] >(({ GET }) => [ GET((ctx) => { // ctx.validated.params is typed and validated at runtime // [!code hl] const { id, activity } = ctx.validated.params }) ]); ``` --- --- url: /.vitepress/theme/components/ResponseValidation.md --- ```ts [api/pages/[id]/index.ts] import { defineRoute } from "_/api"; type Page = { id: VRefine; title: VRefine; content: string; tags: string[]; status: "draft" | "published" | "scheduled"; } export default defineRoute<"pages/[id]">(({ GET }) => [ GET<{ response: [200, "json", Page], // [!code hl] }>(async (ctx) => { // response should match defined schema }) ]); ``` --- --- url: /.vitepress/theme/components/PayloadValidation.md --- ```ts [api/users/index.ts] import { defineRoute } from "_/api"; type Payload = { email: VRefine; password: VRefine; name: VRefine; dateOfBirth: VRefine; agreeToTerms: boolean; marketingOptIn?: boolean; } export default defineRoute<"users">(({ POST }) => [ POST<{ json: Payload, // [!code hl] }>((ctx) => { // ctx.validated.json is typed and validated at runtime // [!code hl] const { email } = ctx.validated.json; }) ]); ``` --- --- url: /.vitepress/theme/components/ComposableMiddleware.md --- ::: code-group ```ts [api/use.ts] import { use } from "_/api"; export default [ use( (ctx, next) => { // global logger // [!code hl] return next(); }, { slot: "logger", }, // [!code hl] ), ]; ``` ```ts [api/example/index.ts] import { defineRoute } from "_/api"; export default defineRoute<"example">(({ use, GET }) => [ use( async (ctx, next) => { // custom logger // [!code hl] return next(); }, { slot: "logger", }, // [!code hl] ), GET(async (ctx) => { // ... }), ]); ``` ```ts [api/env.d.ts] export declare module "@kosmojs/api" { interface UseSlots { logger: string; // [!code hl] } } ``` ::: --- --- url: /.vitepress/theme/components/CascadingMiddleware.md --- ```ts [api/users/use.ts] import { use } from "_/api"; export default [ use(async (ctx, next) => { // any route inside users/ folder and its subfolders // will wire this middleware automatically return next(); }) ]; ``` --- --- url: /validation/error-handling.md description: >- Handle ValidationError instances with detailed error information including scope, error messages, field paths, and structured ValidationErrorEntry data. --- When validation fails - on parameters, request payload, or response - `KosmoJS` throws a `ValidationError` with detailed information about what went wrong and where. Your `api/errors.ts` is the central place to handle it. The generated file gives you a working default; customize it freely to add logging, change response formats, or handle specific error types differently. ## 📦 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 = 400; 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) { 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); }, ); ``` ::: ## 🔧 ValidationError Properties ```ts export class ValidationError extends Error { public target: ValidationTarget; // which part of the request failed public errors: Array = []; public errorMessage: string; // all errors as a single readable string public errorSummary: string; // e.g. "2 validation errors found across 2 fields" public route: string; public data: unknown; // the data that failed validation } export type ValidationErrorEntry = { keyword: string; // JSON Schema keyword that triggered the error path: string; // path to the invalid field message: string; // human-readable description params?: Record; // constraint details, e.g. { limit: 5 } code?: string; // optional code for i18n / custom handling }; ``` The `target` property tells you exactly which part of the request failed: `"params"`, `"query"`, `"headers"`, `"cookies"`, `"json"`, `"form"`, `"raw"`, or `"response"`. **Common handling patterns:** ```ts // All errors as a readable string if (error instanceof ValidationError) { const { target, errorMessage } = error; // e.g. "Validation failed: user: missing required properties: "email", "name"; // password: must be at least 8 characters long" ctx.status = 400; ctx.body = { error: errorMessage }; } // Field-level errors (useful for form responses) if (error instanceof ValidationError) { const messages = error.errors.map(e => `${e.path}: ${e.message}`); ctx.status = 400; ctx.body = { error: "validation_error", target: error.target, messages }; } // Log the invalid data, return a summary if (error instanceof ValidationError) { logger.error("Validation failed", { target: error.target, data: error.data }); ctx.status = 400; ctx.body = { error: error.errorSummary }; } ``` ## 🎨 Custom Error Messages The second type argument to your handler also accepts custom error messages per target. Use `error` as a fallback and `error.fieldName` for field-specific overrides: ```ts [api/users/index.ts] export default defineRoute<"users">(({ POST }) => [ POST< { json: { id: number; email: string; age: number; }; }, { json: { error: "Invalid user data provided", "error.id": "User ID must be a valid number", "error.email": "Please provide a valid email address", "error.age": "Age must be a number", }; } >(async (ctx) => { const { id, email, age } = ctx.validated.json; }), ]); ``` Each target has its own message set. For nested fields, use dot notation: ```ts { json: { error: "Invalid order data", "error.order.items": "Order must contain at least one item", "error.order.shipping.address.postalCode": "Invalid postal code format", } } ``` When validation fails, `KosmoJS` uses the most specific message available - field-specific first, falling back to the generic `error` if no match is found. Custom messages appear in the `message` field of each `ValidationErrorEntry`, so your existing error handler picks them up automatically. --- --- url: /validation/naming-conventions.md description: >- Avoid TypeScript built-in type names when defining types for validation. Use suffix T or prefix T conventions to prevent runtime validation failures. --- Avoid naming your types after TypeScript/JavaScript built-ins like `Event`, `Response`, `Request`, or `Error`. These names compile fine, but cause silent failures during runtime validation. ## ⚠️ Why This Matters When `KosmoJS` flattens types for schema generation, built-in names are referenced as-is rather than resolved to your custom definition. The validator sees the built-in, not your type, and validation fails at runtime without a compile-time warning. ```ts // ❌ Compiles fine, breaks at runtime type Event = { id: number; name: string; timestamp: string }; // ✅ Works correctly type EventT = { id: number; name: string; timestamp: string }; type TEvent = { id: number; name: string; timestamp: string }; // also fine ``` Use a consistent `T` suffix (`EventT`, `ResponseT`) or prefix (`TEvent`, `TResponse`) throughout your project. If validation fails unexpectedly despite correct type definitions, a naming conflict is the first thing to check. ## 📋 Common Built-ins to Avoid **DOM:** `Event`, `Element`, `Document`, `Window`, `Node`, `HTMLElement`, `EventTarget`, `CustomEvent` **Web APIs:** `Response`, `Request`, `Headers`, `Body`, `Blob`, `File`, `FormData`, `URLSearchParams`, `WebSocket` **JavaScript:** `Error`, `Date`, `RegExp`, `Promise`, `Symbol`, `Map`, `Set`, `Array`, `Object`, `String`, `Number` **TypeScript utilities:** `Partial`, `Required`, `Readonly`, `Pick`, `Omit`, `Record`, `Exclude`, `Extract`, `NonNullable` **Node.js:** `Buffer`, `Stream`, `EventEmitter`, `Timeout` For the full list, see the [TFusion builtins reference](https://github.com/sleewoo/tfusion/blob/main/src/builtins.ts). --- --- url: /validation/performance.md description: >- Understand KosmoJS validation performance with TypeScript compiler analysis, intelligent caching, and background processing that doesn't impact development workflow. --- Schema generation uses TypeScript's compiler API to trace your types - including all referenced files - and build a complete dependency graph. This is what makes pure-TypeScript validation possible, with a brief generation step as the tradeoff. Generation time scales with type complexity. Simple routes are near-instant; routes with deep hierarchies and many dependencies may take a few seconds. In practice this rarely affects you - generation runs in parallel with the Vite dev server, and results are cached per file. Schemas only regenerate when the route file or one of its type dependencies changes. By the time you've saved a file and switched to the browser, the schema is ready. ## 🔄 When It Becomes Noticeable Full rebuilds happen in a few specific situations: * Deleting the `lib` folder manually * `KosmoJS` releasing an update that bumps the cache version For large projects with many routes, a full rebuild can take several minutes. This is the same category of thing as clearing `node_modules` or regenerating a Prisma client - infrequent and expected, not part of the normal edit-test cycle. ## ⚖️ Machine Time vs Human Time Zod, Yup etc. have zero generation overhead - because you write the schemas yourself. That eliminates generation time but adds an ongoing maintenance cost. `KosmoJS` trades a few seconds of machine time for eliminating that manual work entirely. For most workflows, that's a good deal. As the `TypeScript` ecosystem evolves - particularly native implementations that [ts-morph](https://ts-morph.com/) and [TFusion](https://github.com/sleewoo/tfusion) may leverage - generation performance will likely improve further. --- --- url: /validation/refine.md description: >- Advanced validation constraints with VRefine using JSON Schema keywords. Validate string formats, numeric ranges, array constraints, and custom patterns directly in TypeScript types. --- `VRefine` adds JSON Schema constraints to a primitive type. It's globally available - no import needed. ```ts VRefine ``` The first argument is the base type, the second is any valid [JSON Schema validation keyword](https://json-schema.org/draft/2020-12/json-schema-validation.html): * **Strings:** `minLength`, `maxLength`, `pattern`, `format` * **Numbers:** `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf` * **Arrays:** `minItems`, `maxItems`, `uniqueItems` One common gotcha: `number` alone allows decimals. If you need a true integer, use `multipleOf: 1` - it means the value must be evenly divisible by 1: ```ts // allows 1000.5 - probably not what you want VRefine // integers only VRefine ``` This matters especially for database IDs, where a float would pass validation but get rejected at the query level - turning a clear validation error into a confusing DB error.