--- url: /essentials/project-structure.md description: >- The anatomy of a KosmoJS project - source folders, the derived lib directory, build output, and the reserved @/ ~/ _/ path mappings. --- A KosmoJS project has four top-level directories, and each one has exactly one job. Once that clicks, the rest of the docs read much faster. ```txt my-app/ ├── package.json -> project settings: distDir, devPort, previewPort, scripts ├── tsconfig.json -> minimal config, add paths to `include` to have them typechecked │ ├── src/ ✍️ YOU WRITE THIS │ ├── front/ -> one source folder │ └── admin/ -> another, fully independent │ ├── lib/ 🤖 DERIVED - never edit │ ├── tsconfig.json -> base config the root tsconfig extends │ ├── front/ -> derived code for src/front │ └── admin/ -> derived code for src/admin │ ├── dist/ 📦 BUILD OUTPUT │ ├── run.js -> runs every folder, one process │ ├── front/ │ └── admin/ │ └── var/ 🗑️ Vite cache - disposable ``` The rule of thumb: **you own `src/`, KosmoJS owns `lib/`.** Every `_/` import you write points into `lib/`. ## Inside a Source Folder A source folder is a self-contained app. This is one with a Hono backend and a React frontend, with SSR enabled - the fullest shape: ```txt src/front/ ├── kosmo.config.ts -> what this folder is ├── tsconfig.json -> { "extends": "../../lib/front/tsconfig.json" } ├── index.html -> Vite's HTML entry ├── app.tsx -> global wrapper, wraps EVERY route ├── router.ts -> routerFactory: routes -> native router │ ├── api/ ── server side ─────────────────────────── │ ├── app.ts -> the backend app instance (appFactory) │ ├── server.ts -> standalone server entry │ ├── dev.ts -> dev hooks: requestHandler, teardownHandler │ ├── errors.ts -> THE central error handler │ ├── use.ts -> global middleware (runs for every route) │ ├── env.d.ts -> global context/state types, custom UseSlots │ └── users/ │ └── [id]/ │ ├── index.ts -> the route -> /api/users/:id │ └── types.ts -> colocated helper, NOT a route │ ├── pages/ ── client side ─────────────────────────── │ ├── 404.tsx -> rendered for unmatched routes │ ├── index/ │ │ └── index.tsx -> the route -> / │ └── users/ │ ├── layout.tsx -> wraps everything under /users │ └── [id]/ │ └── index.tsx -> the route -> /users/:id │ ├── components/ │ └── Link.tsx -> type-safe navigation component │ └── entry/ ├── client.ts -> mount / hydrate in the browser └── server.ts -> renderToString / renderToStream (SSR only) ``` Things worth noticing: * **`api/` and `pages/` are siblings** with parallel trees, so an endpoint and its page are always one folder apart. Neither directory name appears in a URL. * **Only `index.*` is a route.** Everything else in a route folder is a colocated helper. [Rationale ›](/routing/rationale) * **`app.*` is not a layout.** It sits at the folder root and wraps every route; `layout.*` files live inside `pages/**/*` and wrap a subtree. [Details ›](/frontend/layouts#global-layout-via-app-file) * **`use.ts` is not a route either.** Drop one in any `api/` folder and it wraps that subtree. [Details ›](/backend/cascading-middleware) A folder created with `--no-backend` simply has no `api/`; one created with `--no-frontend` has no `pages/`, `app.*`, `router.ts`, `index.html` or `entry/`. ## Inside `lib/` You never edit `lib/`, but knowing what lives there makes the `_/` imports legible. This is a React + Hono folder; exact filenames vary by framework, while the `_/` names you import stay the same: ```txt lib/ ├── tsconfig.json -> shared base for the root tsconfig ├── .gitignore -> why lib/ is NOT in the root .gitignore └── front/ ├── tsconfig.json -> this folder's jsxImportSource + path mappings ├── api.ts -> _/api defineRoute, use ├── api:factory.ts -> _/api:factory appFactory, devSetup, errorHandlerFactory ├── app.tsx -> _/app the AppProvider seam ├── router.tsx -> _/router routerFactory, createRouters ├── query.ts -> _/query TanStack client (when enabled) ├── use.ts -> _/use framework hooks (Vue/Svelte/MDX) ├── entry/ │ ├── client.ts -> _/entry/client │ └── server.ts -> _/entry/server ├── fetch/ -> _/fetch typed clients + ResponseT └── @api/ └── routes.ts -> the RouteMap ``` ::: tip `lib/` has its own `.gitignore` - don't add it to the root one The root `.gitignore` deliberately leaves `lib/` alone, because `lib/.gitignore` handles it more precisely: it ignores **everything** except `cache.json` and `types.ts` at any depth. So derived code is *not* committed - it is a build artifact. What is committed is the per-route derivation cache (`cache.json`, keyed by content hashes of the route file and its type dependencies), so a fresh clone or a CI run doesn't pay for a full rebuild. Adding `lib/` to the root ignore file would drop that cache and make every clone slow. ::: Deleting `lib/` is safe but not free: it forces a full rebuild, which on a large project takes minutes. [Details ›](/validation/performance#when-it-becomes-noticeable) ## Path Mappings Three prefixes are reserved. Don't reuse them for your own aliases. | Prefix | Resolves to | Use it for | |---|---|---| | `@/*` | project root | anything shared across source folders - db layer, domain types | | `~/*` | **this** source folder | your own modules inside the folder | | `_/*` | `lib//` | derived code | ```ts import { db } from "@/db"; // my-app/db.ts import type { User } from "~/types/user"; // src/front/types/user.ts import { defineRoute } from "_/api"; // lib/front/api.ts import fetchClients from "_/fetch"; // lib/front/fetch/ ``` `~/` and `_/` are **folder-relative**: the same `_/api` in `src/admin` resolves to `lib/admin/api.ts`. That is what keeps folders isolated - `admin`'s route names and navigation types never leak into `front`. `@/` is how folders share without packages. Put a type in `db/models.ts`, import it as `@/db/models` from every folder, change it once, and every folder sees it - no workspaces, no publishing, no version bumps. ## Build Output ```txt dist/front/ ├── api/ │ ├── app.js -> the app instance, for custom mounting │ └── server.js -> ready-to-run API server ├── client/ │ ├── assets/ -> hashed JS, CSS, images │ └── index.html └── ssr/ -> only when SSR is enabled ├── app.js ├── server.js -> serves pages AND the API ├── assets/ -> hashed, served at /assets/ └── public/ -> copy of public/, served at / ``` The simplest way to run all of this is `node dist/run.js`, which dispatches across every folder in one process. [Details ›](/dev-build-run/building-for-production#one-entry-point-for-the-whole-project) Folders can also be deployed separately, in which case what you deploy depends on the folder's rendering mode - and for SSR folders it is **not** everything in the directory. [Details ›](/dev-build-run/building-for-production#what-to-deploy) ## Multiple Folders The point of the whole layout. Three apps, one install, one set of types/helpers: ```txt src/ ├── marketing/ MDX, no backend, base "/" ├── app/ React + Hono, base "/app" └── admin/ Vue + H3, base "/admin" ``` Each has its own `kosmo.config.ts`, its own routes, middleware, layouts and build. They share one `package.json`, one `node_modules`, and one `@/` root. ```sh pnpm dev # all folders pnpm dev app # just one pnpm build admin # build one, deploy it independently ``` [Configuration reference ›](/essentials/config) · [Why source folders ›](/about) --- --- url: /essentials/config.md description: >- Complete reference for kosmo.config.ts - the frontend, backend and validation blocks, base URLs, stack plugins and vite options, and the project-level settings in package.json. --- Every source folder owns a `kosmo.config.ts`. It is the one file that decides what that folder *is* - which frameworks it runs, where it is served from, and what gets built for it. ```txt my-app/ ├── package.json <- project-level settings └── src/ ├── front/ │ └── kosmo.config.ts <- this folder's config └── admin/ └── kosmo.config.ts <- independent of front's ``` There is no project-wide `kosmo.config.ts`, and no `vite.config.ts`: each side of the folder carries its own `viteConfig`. ## The Shape The config is declarative - you describe what the folder has: ```ts [src/app/kosmo.config.ts] import { defineConfig } from "@kosmojs/dev"; export default defineConfig({ frontend: { stack: "react", // or solid, vue, svelte, mdx base: "/", fetch: true, ssr: true, ssg: false, tanstack: { query: false }, }, backend: { stack: "hono", // or h3, koa base: "/api", }, validation: true, }); ``` Three top-level keys, all optional: `frontend`, `backend`, `validation`. A folder can have both sides, or just one. Every feature key follows the same pattern: a plain value turns it on with defaults, or an object turns it on and hands KosmoJS the instance to use - `stack` takes a `plugin`, `ssr` / `ssg` / `fetch` / `validation` take a `generator`. `defineConfig` turns that description into the right set of generators, in the right order, and is the only import a folder config needs. ## frontend ### frontend.stack - required `"react"` · `"solid"` · `"vue"` · `"svelte"` · `"mdx"` A bare name runs that stack's Vite plugin with defaults: ```ts stack: "react" ``` To configure it, construct the plugin yourself and pass it alongside the name: ```ts import react from "@vitejs/plugin-react"; stack: { name: "react", plugin: react({ jsxRuntime: "automatic" }), } ``` `name` is what KosmoJS routes on - which stack runs, which page extensions it watches, the `jsxImportSource` it writes into your tsconfig. `plugin` is handed to Vite as you built it, alongside anything KosmoJS adds for that stack. The default plugin each stack resolves to: | stack | default plugin | |---|---| | react | `@vitejs/plugin-react` | | solid | `vite-plugin-solid` | | vue | `@vitejs/plugin-vue` | | svelte | `@sveltejs/vite-plugin-svelte` | | mdx | `@mdx-js/rollup`, with a basic set of `remarkPlugins` | Bring your own plugin instance: ```ts import mdx from "@mdx-js/rollup"; stack: { name: "mdx", plugin: mdx({ remarkPlugins: [frontmatterPlugin, mdxFrontmatterPlugin], rehypePlugins: [rehypeSlug], }), } ``` ::: warning Don't also list the plugin in `viteConfig.plugins` Whichever form you use, the plugin reaches Vite through `stack`. Adding it to `viteConfig.plugins` as well runs the transform twice. ::: ### frontend.base - required The URL prefix this folder's pages are served from. Must be absolute: ```ts base: "/" // app at the root base: "/admin" // admin dashboard under /admin ``` Duplicate slashes are collapsed and a trailing slash is stripped, so `"/admin/"` and `"//admin"` both resolve to `"/admin"`. Path traversal segments (`../`, `/./`) are rejected at startup. ### frontend.fetch Typed [fetch clients](/fetch/intro) in `_/fetch`. ```ts fetch: true ``` Clients are derived from the backend's routes, so this only produces anything when the folder also has a `backend`. ### frontend.ssr [Server-side rendering](/frontend/server-side-render). Accepts `true`, or an options object: ```ts ssr: true ssr: { renderMode: { "docs/**": "stream", }, } ``` **`renderMode`** - `"string"` (default), `"stream"`, or a glob map for per-route selection. [Details ›](/frontend/server-side-render#selecting-the-render-mode) ### frontend.ssg [Static site generation](/frontend/static-site-generation). ```ts ssg: true ``` Renders routes to static HTML at build time. Requires `ssr: true` - the scaffolder turns SSR on for you when you ask for SSG. Dynamic routes declare their variants with `staticParams`. ### frontend.tanstack ```ts tanstack: { query: true } ``` Deploys the `_/query` runtime, swaps `_/app` for a provider that supplies the query client, and gives each SSR request its own client. [Details ›](/frontend/tanstack-query) ### frontend.templates Overrides seeded page boilerplate by route pattern: ```ts templates: { "landing/*": landingTemplate, "marketing/**": landingTemplate, } ``` [Custom Page Templates ›](/frontend/custom-templates) ### frontend.viteConfig Vite's `UserConfig` for the client build - `plugins`, `resolve`, `css`, `server`, `define`, `optimizeDeps`, and the rest: ```ts frontend: { stack: "react", base: "/", viteConfig: { plugins: [tailwindcss()], resolve: { alias: { "#shared": "/src/shared" }, }, css: { preprocessorOptions: { scss: { api: "modern" } }, }, }, } ``` A handful of Vite keys are **not** accepted, because KosmoJS derives them from the source-folder layout: `root`, `base` (the folder's prefixes come from `frontend.base` / `backend.base`), `cacheDir`, `mode`, `builder`, `future`, `legacy`. ## backend ### backend.stack - required `"hono"` · `"h3"` · `"koa"` A bare name, or the same object form the frontend takes. The backend stacks have no Vite plugin, so the object carries only `name` today and the bare name is the usual form. Vite settings for the API build go in [viteConfig](#backend-viteconfig). ### backend.base - required The URL prefix this folder's API routes are served from - a **full path**, resolved on its own rather than against `frontend.base`: ```ts frontend: { base: "/vue" }, backend: { base: "/vue/api" }, // routes at /vue/api/ ``` Nesting it under the frontend base is the convention the scaffolder follows, but nothing requires it - the two prefixes are independent: ```ts frontend: { base: "/admin" }, backend: { base: "/api/v2" }, // routes at /api/v2/ ``` A route's final URL is `backend.base` + route name: ``` base "/api" route "users/[id]" -> /api/users/:id base "/admin/api" route "users/[id]" -> /admin/api/users/:id base "/v1" route "users/[id]" -> /v1/users/:id ``` The `api/` directory name never appears in the URL - it separates server routes from `pages/` on disk, nothing more. ### backend.openapi Derives an [OpenAPI 3.1 spec](/openapi) from this folder's routes. Options are required: ```ts backend: { stack: "hono", base: "/api", openapi: { outfile: "openapi.json", openapi: "3.1.0", info: { title: "My API", version: "1.0.0" }, servers: [{ url: "https://api.example.com/api" }], }, } ``` [Details ›](/openapi#configuration) ### backend.alias Maps a public URL to an existing named route: ```ts alias: { "/feed.xml": "rss", // /feed.xml handled by the "rss" route "/members/[id]": "users/[id]", // param names must match exactly } ``` The key is absolute and is *not* prefixed by the router's base. If it carries dynamic segments, their names must match the target route's parameters exactly, or the request 404s. [Details ›](/backend/aliases) ### backend.templates Overrides the seeded route boilerplate by route-name pattern - the route file (`defineRoute(...)`), not a page component. This is what makes it useful for seeding CRUD endpoints across many tables at once. ```ts templates: { "admin/**": adminRouteTemplate, } ``` [Details ›](/backend/custom-templates) ### backend.viteConfig Vite's `UserConfig` for the API build, with the same exclusions as the frontend's: ```ts backend: { stack: "hono", base: "/api", viteConfig: { define: { __API_BUILD__: true }, }, } ``` The two sides are built separately, so `frontend.viteConfig` and `backend.viteConfig` are independent. ## validation Runtime [validators derived from your types](/validation/intro). Only meaningful alongside a `backend` - it validates incoming requests. ```ts validation: true ``` For anything beyond on/off, pass an options object instead: ```ts validation: { // override validation messages - node:util.format placeholders validationMessages: { STRING_MIN_LENGTH: "must be at least %d character%s long", NUMBER_MULTIPLE_OF: "must be a multiple of %s", }, // file whose default export maps custom TypeBox types customTypesImport: "@/validation/types.ts", // identifier used for runtime refinements, default "VRefine" refineTypeName: "Refine", settings: { maxErrors: 8, // cap buffered diagnostics (DoS guard) useEval: true, // disable where unsafe-eval is blocked by CSP exactOptionalPropertyTypes: false, immutableTypes: false, }, } ``` * **`validationMessages`** is the place for i18n or project wording - it changes every message globally, unlike the per-field [custom error messages](/validation/error-handling#custom-error-messages) you set on a handler. * **`refineTypeName`** renames `VRefine` if it collides with something in your codebase. The name is global and import-free either way. [Details ›](/validation/refine) * **`settings.useEval: false`** is the option to reach for when a strict Content Security Policy forbids `unsafe-eval`; validation falls back to dynamic checking. * **`settings.exactOptionalPropertyTypes: true`** aligns runtime check semantics with the TypeScript flag of the same name. ## What the scaffolder writes Rather than assembling this by hand, let [kosmo folder](/cli/folder) write the right config for your answers - interactively, or from flags. It names the bases after the folder: `/` for the frontend, `//api` for the backend. For reference, these are the configs it produces for a folder named `front`: ### What the scaffolder writes - React + Hono ```ts // React + Hono import { defineConfig } from "@kosmojs/dev"; export default defineConfig({ frontend: { stack: "react", base: "/front", fetch: true, ssr: false, ssg: false, tanstack: { query: false }, }, backend: { stack: "hono", base: "/front/api", }, validation: true, }); ``` ### What the scaffolder writes - Frontend only ```ts // Frontend only import { defineConfig } from "@kosmojs/dev"; export default defineConfig({ frontend: { stack: "react", base: "/front", fetch: true, ssr: false, ssg: false, tanstack: { query: false }, }, }); ``` ### What the scaffolder writes - Backend only ```ts // Backend only import { defineConfig } from "@kosmojs/dev"; export default defineConfig({ backend: { stack: "koa", base: "/front/api", }, validation: true, }); ``` ### What the scaffolder writes - MDX docs ```ts // MDX docs import { defineConfig } from "@kosmojs/dev"; export default defineConfig({ frontend: { stack: "mdx", base: "/docs", fetch: true, ssr: true, ssg: true, }, }); ``` ### What the scaffolder writes (cont.) > Changing what a folder has - adding `ssr`, a `backend`, `validation` - requires a > **dev server restart**. The config is read once at startup. ## Bringing your own generator Each block accepts a `generator` key that replaces the built-in one for that slot. This is the escape hatch for a framework or a validator KosmoJS does not ship: ```ts frontend: { stack: "react", base: "/", generator: myReactGenerator(), fetch: { generator: myFetchGenerator() }, ssr: { generator: mySSRGenerator() }, }, backend: { stack: "hono", base: "/api", generator: myHonoGenerator(), openapi: { generator: myOpenapiGenerator() }, }, validation: { generator: myValidationGenerator() }, ``` The order generators run in is fixed and does not depend on how you write the config: ```txt core -> backend -> validation -> openapi -> fetch -> frontend -> ssr -> ssg ``` `coreGenerator` always runs first and is never listed. ## Project Settings - `package.json` A few settings are project-wide rather than per-folder, and live in the root `package.json`: ```json [package.json] { "type": "module", "distDir": "dist", // [!code hl:3] "devPort": 4556, "previewPort": 4558, "scripts": { "dev": "kosmo serve", "build": "kosmo build", "preview": "kosmo preview", "typecheck": "kosmo typecheck", "folder": "kosmo folder" } } ``` | Field | Default | Meaning | |---|---|---| | `distDir` | `"dist"` | Build output directory for every folder | | `devPort` | `4556` | Port the dev server listens on | | `previewPort` | `4558` | Port [kosmo preview](/dev-build-run/production-preview) listens on | > Changing `distDir` also means updating `.gitignore`, which the scaffolder points at the default `/dist/`. Four scripts take optional folder names - `pnpm dev front`, `pnpm build admin`, `pnpm preview front`, `pnpm typecheck admin front` - and act on every source folder when given none. `previewPort` is separate from `devPort` so preview and the dev server can run at the same time. ## TypeScript Config ### Root tsconfig.json The project root has a `tsconfig.json` covering anything you keep outside `src/`. It starts with only the ambient declarations in scope, so add the paths you want [typechecked](/cli/typecheck): ```json [tsconfig.json] { "extends": "./lib/tsconfig.json", "include": ["./lib/*.d.ts"] } ``` The root `include` is for code nothing imports - a migration script, a worker, a standalone config, a helper you only ever run by hand. Those have no importer to pull them in, so they are checked only if you list them here. Shared code is the other way round. Anything a source folder imports is checked along with that folder, so it does not need listing - adding it here only means it is checked on a root run too, rather than only when its importer is. When you add such a path, add to the list rather than replacing it. This is **essential**: `include` replaces rather than merges with the config it extends, so `./lib/*.d.ts` has to stay in it. > Merging extended arrays has been [requested since 2017](https://github.com/microsoft/TypeScript/issues/20110) > and is still open, so carrying the entries over by hand is the only option. *** ::: warning Never add `lib/` itself `./lib/*.d.ts` matches only the ambient declarations at the top of `lib/` - that entry is meant to be there. The rest is derived code, generated against each folder's own path mappings and already checked by that folder's run. In the root program `_/` resolves against the wrong folder, so you get errors in files you cannot fix - they are rewritten on the next run. ::: ### Per-folder tsconfig.json Each source folder has its own `tsconfig.json` extending a derived base in lib dir: ```json [src/front/tsconfig.json] { "extends": "../../lib/front/tsconfig.json" } ``` The base `tsconfig.json` supplies the framework's `jsxImportSource`, the reserved path mappings, and strict compiler settings. Anything you add in your own `compilerOptions` wins, and applies to that folder only: ```json [src/front/tsconfig.json] { "extends": "../../lib/front/tsconfig.json", "compilerOptions": { "exactOptionalPropertyTypes": false } } ``` Note there is no `include` in a source folder's `tsconfig.json`. The base `tsconfig.json` already has everything a source folder normally needs. But if you really need to include more, carry these base entries over first: ```json ["./", "../../lib//", "../../lib/*.d.ts"] ``` So your file should look like this: ```json [src/front/tsconfig.json] { "extends": "../../lib/front/tsconfig.json", "include": ["./", "../../lib/front/", "../../lib/*.d.ts", "../../shared"] } ``` ::: tip You do not need `include` to *use* shared code Anything you import is typechecked along with the file importing it. `import { formatUser } from "@/shared/user"` pulls `shared/user.ts` into that folder's program whatever `include` says - the `@/` prefix resolves it, and there is nothing to add to any `tsconfig.json`. ::: --- --- url: /essentials/frameworks.md description: >- What each backend and frontend framework supports in KosmoJS - mixed segments, power syntax, streaming SSR, SSG, TanStack Query, layouts, data loading and hooks - in one table. --- Routing conventions, validation, middleware composition and the fetch clients are **identical across every framework**. This page is about the places where they aren't - gathered here so you can choose a stack without hunting through six pages of info boxes. ## Backends | | Hono | H3 | Koa | |---|:---:|:---:|:---:| | Runtimes | Node · Deno · Bun · Workers · edge | Node · Deno · Bun · edge | Node (Deno/Bun via `node:http` compat) | | Handler style | return `ctx.json()` / `ctx.text()` | return the value directly | mutate `ctx.body` | | Raw params | `ctx.req.param()` | `event.context.params` | `ctx.params` | | Validated params | `ctx.validated.params` | `ctx.validated.params` | `ctx.validated.params` | | Route-specific types | Variables, Bindings | Context | State, Context | | Global types (`api/env.d.ts`) | `DefaultVariables`, `DefaultBindings` | `DefaultContext` | `DefaultState`, `DefaultContext` | | Error entry point | `app.onError()` | `app.use(onError(...))` | middleware around `await next()` | | [Mixed segments](/routing/params#mixed-segments) | ⚠️ partial | ⚠️ partial | ✅ full | | [Power syntax](/routing/params#power-syntax) | ⚠️ matches, params renamed `_0abc` | ❌ won't match | ✅ full | **Choosing:** take Hono for maximum performance and the widest runtime reach; H3 for the same, with a stronger Web-standards focus; Koa for a mature Node ecosystem - and it is the only backend with complete mixed-segment and power-syntax support. [Details ›](/backend/intro) ## Frontends | | React | SolidJS | Vue | Svelte | MDX | |---|:---:|:---:|:---:|:---:|:---:| | Page extension | `.tsx` | `.tsx` | `.vue` | `.svelte` | `.mdx` / `.md` | | Layout file | `layout.tsx` | `layout.tsx` | `layout.vue` | `layout.svelte` | `layout.mdx` | | Renders children with | `` | `props.children` | `` | `{@render children()}` | `props.children` | | `jsxImportSource` | `react` | `solid-js` | `vue` *(JSX only)* | n/a | `preact` | | Data loading export | `loader` | `preload` | `loader` | `loader` | `loader` | | Reading loaded data | `useLoaderData` *(react-router)* | `createAsync` | `useLoaderData` *(`_/use`)* | `useLoaderData` *(`_/use`)* | `useLoaderData` *(`_/use`)* | | Needs `` | ❌ | ✅ | ❌ | ❌ | ❌ | | [\_/use module](/frontend/hooks) | ❌ | ❌ | ⚠️ `useLoaderData` only | ✅ | ✅ | | [Streaming SSR](/frontend/server-side-render#stream-rendering) | ✅ | ✅ | ✅ | ❌ | ❌ | | [SSG](/frontend/static-site-generation) | ✅ | ✅ | ✅ | ✅ | ✅ | | [TanStack Query](/frontend/tanstack-query) | ✅ | ✅ | ✅ | ✅ | ❌ | | TanStack read hook | `useQuery(opts)` | `useQuery(() => opts)` | `useQuery(opts)` | `createQuery(() => opts)` | – | | [Mixed segments](/routing/params#mixed-segments) | ⚠️ `.ext` suffix only | ❌ | ✅ | ✅ | ✅ | | [Power syntax](/routing/params#power-syntax) | ❌ | ❌ | ❌ | ❌ | ❌ | ### Reading the exceptions * **SolidJS is the only frontend that needs a `` boundary** in the common case. `createAsync` suspends; the other frameworks' loaders resolve before render. KosmoJS ships no boundary for you - scoping it is your call. [Why ›](/frontend/data-preload#suspense-is-your-responsibility) * **Svelte and MDX render to strings only.** They implement `renderToString` but not `renderToStream`, and their folders don't accept the streaming [renderMode](/frontend/server-side-render#selecting-the-render-mode). * **MDX has no client runtime**, so TanStack Query is unavailable there. Fetch with an MDX `loader` instead. * **Svelte does not use SvelteKit.** KosmoJS uses only Svelte's UI layer, so data loading is the `loader` export, not SvelteKit's `load`, and there are no `+page` files. ## Routing Syntax Support The parameter syntaxes are the same everywhere; only the exotic ones vary. | Syntax | Example | Everywhere? | |---|---|---| | Required `[id]` | `users/[id]` | ✅ all backends and frontends | | Optional `{id}` | `users/{id}` | ✅ all backends and frontends | | Splat `{...path}` | `docs/{...path}` | ✅ all backends and frontends | | Mixed segment | `files/[name].[ext]` | ⚠️ see tables above | | Power syntax | `book{-:id}-info` | ⚠️ Koa only | **Practical rule:** keep frontend routes to the three plain syntaxes. Use mixed segments on the API side, where support is complete on Koa and workable on Hono/H3. Reach for power syntax only on a Koa backend. [Parameter details ›](/routing/params) ## Mixing Frameworks Support differences are per **source folder**, not per project - a folder runs exactly one frontend and at most one backend. So the differences above are choices you make per app, not constraints you carry project-wide: ```txt src/ ├── marketing/ MDX + SSG -> static, no backend ├── app/ React + Hono -> SSR, streaming, TanStack Query └── admin/ Vue + H3 -> CSR ``` A folder also ignores other frameworks' files: a Vue folder skips `.tsx`, a React folder skips `.vue`/`.svelte`. [Details ›](/frontend/intro#extensions-per-framework) --- --- url: /essentials/migration-tips.md description: >- Translation guide for developers moving to KosmoJS from Next.js App Router, TanStack Start/Router or tRPC - server actions, route groups, parallel routes, loading.tsx, revalidateTag, next/image, validateSearch, routeTree.gen.ts and middleware.ts, each mapped to its KosmoJS equivalent or explicitly marked as having none. --- If you are arriving from **Next.js App Router**, **TanStack Start/Router** or **tRPC**, most of what you know transfers - and a few things deliberately don't. The one shift worth internalizing first: **the client/server boundary is an HTTP call.** Server code lives in `api/`, client code in `pages/`, and a typed fetch client carries the types across. There is no interleaving of server and client code in one file. During SSR that call runs [**in-process**](/fetch/isomorphic-clients), so the boundary costs nothing on the server. The second shift: **the unit of an app is a [source folder](/essentials/project-structure).** Not a route group or a workspace package. A folder has its own framework, base URL, middleware and build - so several organizational features other frameworks provide inside one app are structural here instead. ## Quick reference | You're looking for | In KosmoJS | |---|---| | `app/page.tsx` | a folder with an [index file](/routing/rationale) - `pages/users/[id]/index.tsx` | | `[id]` / TanStack `$id` | `[id]` - same concept, [required param](/routing/params) | | `[...slug]` / `[[...slug]]` | `{...path}` - one splat [covers both](/routing/params) | | optional segment | `{id}` - same concept, [optional param](/routing/params) | | `layout.tsx` / `_layout` | `layout.*` in a [route folder](/frontend/layouts) | | `not-found.tsx` | `pages/404.*` - a [dedicated](/frontend/error-pages) page for 404 errors | | `loading.tsx` / `error.tsx` | your framework's Suspense / error boundary, at [app.\*](/frontend/application) | | `(group)` route groups | a separate [source folder](/essentials/project-structure#inside-a-source-folder) | | `@slot` parallel / `(.)` intercepting | no equivalent - compose in a layout / use modal state | | `route.ts` route handlers | [defineRoute](/backend/intro#defining-endpoints) in `api/**/index.ts` | | `middleware.ts` | global [api/use.ts](/backend/middleware) per app, or [cascading use.ts](/backend/cascading-middleware) per subtree | | `"use server"` / Server Actions | an API route + its [fetch client](/fetch/intro) | | `createServerFn` | ditto | | tRPC procedures | route name + HTTP method, [typed](/fetch/type-safety) / [validated](/validation/payload) end to end | | Zod schemas | TypeScript types - [validators are derived](/validation/intro) | | `validateSearch` | not implemented; validate [query](/validation/payload) on the API contract | | `routeTree.gen.ts` | nothing to register - filesystem is the [route tree](/routing/intro) | | `revalidatePath` / `revalidateTag` / ISR | no equivalent - cache at CDN/proxy, or SSG | | `next/image`, `next/font` | no equivalent - bring your own | | `next/link` | typed [Link](/frontend/link-navigation) component | | `next/head` / `metadata` | MDX frontmatter, or the SSR entry's `head` | | `next.config.js` | per-folder [kosmo.config.ts](/essentials/config) (it *is* the Vite config) | | `next start` | [node dist/run.js -p 4556](/dev-build-run/building-for-production) | | `.next/` | [dist/\/](/dev-build-run/building-for-production#build-output) | | multi-zone | multiple [source folders](/essentials/project-structure#inside-a-source-folder) in one project | ## Routing ### `app/page.tsx` and why folders A route is a **folder with an `index` file**: `pages/users/[id]/index.tsx` -> `/users/:id`. Only `index` is the route; every sibling file is an obviously-colocated helper. That is the whole reason for the extra folder - at scale, `schema.ts` next to `page.tsx` is ambiguous, and here it never is. [Details ›](/routing/rationale) ### Params Different sigils, same concepts - `[id]` required, `{id}` optional, `{...path}` splat. [Details ›](/routing/params) ### `[[...slug]]` - optional catch-all There is no separate required-vs-optional catch-all. The splat `{...path}` matches **any number of segments including zero**, so `docs/{...path}` matches `/docs` as well as `/docs/a/b/c` - covering both `[...slug]` and `[[...slug]]` with one form. [Details ›](/routing/params#splat-parameters) ### `routeTree.gen.ts` - the central route tree There is no route tree to register or import. Routing is filesystem-driven; the route config is derived per source folder into `lib/` and handed to the framework's native router. Treat it as a build artifact - you never edit or read it. [Details ›](/frontend/routing#routes) ### `(group)` - route groups There is no route-group syntax, and none is needed. Next's `(group)` organizes routes without affecting the URL - a way to separate concerns inside one app. KosmoJS separates concerns one level up: a [source folder](/essentials/project-structure) is an independent app with its own framework, base URL, middleware and build. The separation route groups gesture at is structural here rather than a naming convention. ### `@slot` parallel routes and `(.)` intercepting routes **No equivalent.** One folder maps to one route. * *Parallel routes* render several independent pages into named slots of one layout. Build it by rendering multiple components in a layout and fetching their data independently. * *Intercepting routes* show a route differently depending on how you arrived (photo-in-a-modal vs. the full page). Build it with client-side modal state, or your router's modal patterns. ### `loading.tsx` / `error.tsx` / `not-found.tsx` There are **no per-route special files** for loading and error states - they are handled with each framework's own primitives rather than a KosmoJS file convention. Global loading, Suspense and error boundaries belong at the [app.\*](/frontend/layouts#global-layout-via-app-file) level. Not-found **does** have a built-in: a [pages/404.\*](/frontend/error-pages) component is rendered for unmatched routes. Backend errors are separate and centralize in [api/errors.ts](/backend/error-handling). ::: tip Suspense is your responsibility KosmoJS ships no `` boundary on purpose - one app-wide boundary collapses the whole page to a single fallback. Scope it yourself. [Details ›](/frontend/data-preload#suspense-is-your-responsibility) ::: ### Nested layouts, and do they persist state? Same idea as `layout.tsx` / `_layout`: a `layout` file wraps its folder and subfolders, nesting by folders. And yes - navigating between siblings under one layout swaps only the child, so the layout stays mounted and **its state is preserved**, exactly as in App Router. It remounts only when navigation leaves its subtree. [Details ›](/frontend/layouts) ### `beforeLoad` and `validateSearch` There is no proprietary `beforeLoad`-style hook - your framework's primitives are untouched, so use React Router's `loader`, Solid Router's `preload`, or Vue Router's navigation guards directly. Client-side typed/validated **search params are not implemented yet** - it's a considered feature. Query params are validated on the *API contract* (the `query` target, with `VRefine` constraints, surfaced through the fetch clients), but there is no client-side `validateSearch` typing `useSearch()` on the page route the way TanStack does. For now, read search params with your framework's native router. [Details ›](/validation/payload#validation-targets) ## Data fetching ### React Server Components and `"use server"` **No RSC, and no `"use client"` / `"use server"` directive boundary - by design.** Server code lives in `api/`, client code in `pages/`, and a plain HTTP API sits between them with typed fetch clients across the wire. There is no interleaving of server and client code in one file and no new mental model to learn: the boundary is the network call. And with the isomorphic client that boundary is free on the server - during SSR the call runs in-process, with no network layer at all. [Details ›](/fetch/isomorphic-clients) ### Server functions / `createServerFn` / Server Actions **There aren't any, and you don't need them.** A server function exists to run server-only code from the client without hand-writing an endpoint - which is exactly what an API route plus its typed client already gives you, with validation and OpenAPI included. The same client is isomorphic: in-process during SSR, a same-origin request on the client. Do mutations by defining a normal `POST`/`PUT`/`DELETE` route and calling its client. Note there is no progressive-enhancement no-JS form submit as a first-class feature, and no `useFormState`/`useActionState` equivalent - use your framework's form state plus the client's [validationSchemas](/fetch/validation#validation-schemas) for field errors. ### Can a page read the database directly? **Not the RSC way** - data flows through the API layer rather than direct DB access in a page. During SSR this is not a network hop: the isomorphic client dispatches to the backend route `in-process` (no socket), so you get the API boundary without the round-trip cost. [Details ›](/fetch/isomorphic-clients) ### Loaders, and `loaderDeps` / staleness Loaders work the way you expect: `export loader` on React, Vue, Svelte and MDX, `export preload` on SolidJS - the loader simply being your fetch client's method exported under the name the router expects. There is **no built-in loader cache** and no `loaderDeps`/staleness model. React and Solid reuse the in-flight result for that navigation; Solid's `preload` results are reused by `createAsync`. For real caching, enable [TanStack Query](/frontend/tanstack-query) - a first-class option. [Details ›](/frontend/data-preload#page-integration) ### `revalidatePath` / `revalidateTag` / ISR **No equivalent.** No fetch-cache extensions, no tag or path revalidation, no ISR, and no partial prerendering. Cache at the CDN or proxy layer; use [SSG](/frontend/static-site-generation) for fully static output. After a mutation, refetch or invalidate your own client cache. ### Does SSR data fetching need plumbing? No - fetch clients are isomorphic, and each framework's own hydration carries the result to the client, so nothing re-fetches. You do not wire `dehydrate`/`hydrate` for that. (TanStack Query is an opt-in layer; serializing *its* cache across SSR is on you.) [Details ›](/fetch/isomorphic-clients) ## Backend ### `route.ts` route handlers / `createAPIFileRoute` `defineRoute` in `api/**/index.ts`, returning an array of method handlers - same idea, plus validation and a fetch client for free. You don't write `Response.json()`, and there is no`NextRequest`/`NextResponse`: it's the native Hono/H3/Koa context. [Details ›](/backend/intro#defining-endpoints) ### `middleware.ts` - global edge middleware Global middleware lives in [api/use.ts](/backend/middleware) file. Whatever it default-exports runs for every route in that app, with no registration. ```ts [api/use.ts] import { use } from "_/api"; export default [ use(async (ctx, next) => { // runs for every route under this folder's `backend.base` return next(); }), ]; ``` There is no `middleware.ts`, and nothing is exported from a config file to enable it - the default export of `api/use.ts` is the registration. Below it, a [cascading `use.ts`](/backend/cascading-middleware) in any `api/` subfolder wraps everything beneath it, carrying typed context via `UseT`, and [slots](/backend/middleware#slot-composition) let a route substitute a global default in place. Three differences from Next's `middleware.ts` are worth knowing before you port anything: * **It is not at the edge.** It runs in your API server, in the same process as the handlers - there is no separate edge runtime, and no Web-API-only subset to code against. * **It is per route, not per request.** Global middleware is composed into each route's chain, so a request matching no route never reaches it. Rewrites, redirects for unknown URLs, or blanket header injection belong at the reverse proxy, or on the native app instance in [api/app.ts](/backend/intro#foundation-files). * **It never sees page requests.** `api/use.ts` covers routes under the folder's `backend.base` only. Next's `middleware.ts` intercepts page navigations too; there is no client-route interception layer here. Gate the client side in the global `app.*` wrapper or a [layout](/frontend/layouts) instead - and remember that a client-side check is UX, not security: the API middleware is what actually enforces it. ### Edge / Cloudflare / Deno / Bun With Hono and H3 the API runs on Node, Deno, Bun, Cloudflare Workers and edge platforms unchanged via `app.fetch`; Koa runs through the `node:http` compat layer. There is **no automatic serverless/edge packaging** like Next's - you run the bundled server, or wire `app.fetch` into an edge runtime yourself. [Details ›](/dev-build-run/building-for-production#running-the-api-server) ### Auth There is no bundled auth and no NextAuth integration. Any Hono/H3/Koa middleware works unchanged - verify a token in a `use.ts` and set `ctx.state.user` / `ctx.set("user")`. [Details ›](/backend/cascading-middleware#common-use-cases) ## Validation & types ### "Does it use Zod?" No - and you don't hand-write schemas at all. TypeScript types are converted to JSON Schema and compiled to TypeBox validators automatically, so one type definition drives compile-time types, runtime validation, the fetch client and the OpenAPI spec. There is no schema to drift from your types. [Details ›](/validation/intro) The trade-off worth knowing: you give up schema-level *transforms* and custom refinement functions, and you get constraints declaratively through [VRefine](/validation/refine) instead. Do transformation in the handler, where it's plain code. ### tRPC-style end-to-end type safety Effectively yes, through typed fetch clients - params, payload and response types all derive from the same route definition, with client-side validation before the request. The difference is that it's **route-based** (path key + HTTP method) rather than procedure-based, and it comes with automatic [runtime validators](/validation/intro) and [OpenAPI spec](/openapi), which tRPC does not produce natively. [Details ›](/fetch/intro) ### Compile-time or runtime? Both, from one definition - which is the main difference from TanStack Router's compile-time-only route typing. The derived validators run on real requests. [Details ›](/essentials/why-codegen) ## Rendering The dev server always renders on the client - Vite with HMR, for the fastest feedback loop available. That is the **development** mode, not a rendering philosophy: it says nothing about what a folder ships. In production, each folder picks its own mode: * **CSR** - a static client bundle served alongside the API. * **[SSR](/frontend/server-side-render)** - opt in per folder, then string- or stream-rendered per route. * **[SSG](/frontend/static-site-generation)** - pre-rendered HTML at build time, available on every frontend. The closest thing to `output: export`. Because dev never server-renders, the [preview command](/dev-build-run/production-preview) is the step before you ship: it builds and serves the real production output - server-rendered pages, hashed assets, the production validation policy - on its own port, so it can sit next to the dev server. Migrators trip on this more than anything else, so make preview part of the loop rather than a last resort. There is **no ISR, no on-demand revalidation and no partial prerendering**, and islands / partial hydration is not offered as a named feature. For ``: MDX frontmatter drives it, or the SSR entry's returned `head`. There is no `metadata` / `generateMetadata` export convention. ## Project & tooling ### `next.config.js` Per-folder [kosmo.config.ts](/essentials/config). Vite's `UserConfig` goes in `viteConfig` on the `frontend` and `backend` blocks - `plugins`, `resolve`, `css` and the rest. There is no separate `vite.config.ts` and no project-wide kosmo config. ### `.next/` and `next start` Build output is `dist//` with `api/`, `client/`, `ssr/` and `ssg/` subdirectories, plus a `dist/run.js` dispatcher. The `next start` equivalent is `node dist/run.js -p 4556` - one process serving every source folder. It's `node:http`, so `bun` and `deno run -A` work too. [Details ›](/dev-build-run/building-for-production) ### `next/image` and `next/font` **No equivalent** - there is no image optimization or font pipeline. Bring your own. ### `next/link` The typed [Link](/frontend/link-navigation) component whose `to` prop is a `[routeName, ...params]` tuple, so renaming a route directory turns every stale link into a compile error. ### Multi-zone The source-folder model *is* the multi-app story: per-folder base URLs, frameworks and builds inside one project, sharing types and a database layer directly. Where Next stitches separate deployments together with multi-zone, KosmoJS keeps the apps in one codebase with no zone configuration. ### Deployment lock-in None. It's a standard Node/Vite app - deploy the bundled servers to Node, Bun, Deno or the edge yourself. You can deploy to Vercel as a Node app, but there are no Vercel-specific features and no dependence on them. ## What you gain Worth naming, since it's the other half of the trade: derived **runtime validators** from your TypeScript types, **typed fetch clients** with client-side validation, automatic **OpenAPI 3.1**, and **multi-app orchestration** in one project - several frameworks side by side, sharing types with no workspace protocols, each building and deploying independently. [What KosmoJS is ›](/about) · [Feature overview ›](/features) --- --- url: /essentials/why-http.md description: >- The industry spent two decades pulling server logic out of the page, and is now putting it back? A look at what actually moved, and why KosmoJS keeps an explicit HTTP boundary while making it cost nothing during SSR - one isomorphic fetch client, two transports, validation on both ends. --- A fair question, asked more than often: > The whole industry is moving server logic back **into the page** - server components, `"use server"`, server functions. > Why does KosmoJS still make me define an API route and call it over HTTP? It is a good question, and it deserves more than "because REST". The honest answer starts with a bit of déjà-vu. ## We have been here before Anyone who was writing web software in the early 2000s remembers what a page looked like: ```php

``` One file. Server logic, database access, markup and styles, colocated by default because there was nowhere else to put them. It was **fast to write** - that part was never in dispute - and every interaction cost a full page reload. Then the page learned to talk back without reloading. `XMLHttpRequest` arrived quietly - shipped by a browser vendor for its own webmail product, which wanted to update part of a page instead of all of it - and stayed a curiosity for years, worked around with hidden iframes by the few who cared. What ended the obscurity was the free webmail service that opened, invite-only, in 2004. A mailbox in a browser that behaved like a desktop application: threads expanding, messages sending, labels applied, none of it costing a full-page reload. It was the first time a mainstream audience used the technique daily without knowing it existed. A mapping app that dragged instead of paginating followed a year later, and in 2005 the whole approach finally got the name everybody used for the rest of the decade: `AJAX`. What followed was twenty years of the same move, repeated: | Step | What moved out of the page | |---|---| | XHR, then JSON over XML | The *rendering* of updates - server started answering with data | | REST, then SPAs | The *routing and state* - the page became an application | | A second client (mobile, then partners) | The *contract* - the API outlived the UI that prompted it | | OpenAPI, GraphQL, typed clients | The *types* - the boundary became something you could check | By the mid-2010s the shape had settled into something almost nobody had to argue for: a rich client, a slim API, and a well-understood protocol between them. Not because it was elegant, but because it survived contact with second clients, third-party integrations, mobile teams, rate limits, caching layers, incident postmortems and audits. And now the pendulum swings back: a component reads the database, a function marked `"use server"` is called as if it were local, and the file looks - squint a little - like that PHP page again. ## So what actually changed? Here is where innocent questions are more useful than opinions. The obvious reading is "we undid twenty years of work". That reading is wrong, and worth dismissing carefully: **the boundary did not go away**. A `"use server"` function still becomes an endpoint. The compiler writes it, gives it a generated id, serializes arguments and results across the same network the PHP page did not have. The client/server split is fully intact - it moved from something you *write* to something the framework *emits*. That is a real **ergonomic win**, and it solves a real annoyance: hand-writing an endpoint, a client, and the types on both sides, three times, for one button. So the question isn't "server or page". It is narrower and more interesting: > When the boundary becomes invisible, what else becomes invisible with it? A few things that used to be obvious: * **What is the URL?** Generated ids are not addresses you can share, curl, or put in a runbook. * **Who else can call it?** A mobile app, a partner integration, a cron job, a support script - none of them import your React components. * **What runs at the boundary?** Auth, rate limiting, request logging, tracing, validation - the things that live in middleware because they must apply to *everything* crossing the line. * **What does the wire look like?** Payload shapes you can inspect in a proxy, in a log, in a HAR file from a user's browser. * **What can be deployed separately?** A public API in a DMZ and an admin UI behind a VPN are one process, or two, depending on whether there is a boundary to cut along. None of that is an argument that colocation is wrong. It is an argument that the *explicitness* of the boundary was doing work that people mostly noticed when it was gone. Which leads to the question this framework is an answer to: > Colocation is nice because the boundary is cheap to cross. > Explicitness is nice because the boundary is real. Do you actually have to pick one? ## KosmoJS's answer: keep the boundary, delete the latency Server code lives in `api/`, client code in `pages/`, and an HTTP API sits between them. Every endpoint has a URL. You can curl it, log it, proxy it, hand it to a mobile team, put it in an [OpenAPI spec](/openapi), and deploy it on its own. The part that makes this cheap is the [isomorphic fetch client](/fetch/intro) derived from each route - meaning the same call site works in the browser and on the server, and only the transport underneath differs: ```ts import fetchClients from "_/fetch"; const { GET } = fetchClients["users/[id]"]; const user = await GET([123]); // identical code in a component and in a loader ``` | Where it runs | Transport | Cost of the boundary | |---|---|---| | Browser, after hydration | Global `fetch`, same-origin request | A normal HTTP request - as it should be | | Server, during SSR | Direct dispatch into the API app | A function call and an object. **No socket, no localhost hop, no round trip.** | During SSR the API is bundled *into* the SSR server, and the fetch client hands a real `Request` straight to the backend app instance. What runs is not a shortcut around your API: it is your API, complete with routing, [middleware](/backend/middleware), validation, error handling and response shaping. Details worth knowing about that in-process path: * **Headers from the incoming page request are forwarded as defaults**, so cookies and auth headers reach the route exactly as they would over the network. Anything you set on the call itself wins. * **Redirects are followed in-process**, including the `303`/`301`/`302` rewrite to `GET`, up to the same five hops the fetch spec allows. * **Native `fetch` is not patched.** Only the fetch clients switch transports; every other fetch in your app behaves exactly as it always did. * **Nothing about the call site changes.** A loader is a loader; the framework's own data model - Solid's `createAsync`, React Router's `loader`, `useLoaderData` in Vue, Svelte and MDX - is what you write. [Details ›](/fetch/isomorphic-clients) So the trade the industry seems to be posing - *keep the architecture and pay a round trip, or drop the architecture and get the speed* - turns out not to be the only option on the table. The round trip was never the point of the boundary; it was just the usual way of crossing it. ::: tip Where the fetch fires still matters In-process dispatch applies to requests made **during rendering** - loaders, preloads, `createAsync`. A fetch in `useEffect` / `onMounted` runs after hydration, in the browser, over the network - because that is where and when it naturally happens. ::: ## Declared once, enforced on both ends The other thing an explicit boundary buys is a place to state what may cross it. Validation is declared in the route, as ordinary TypeScript types, next to the handler that relies on it. From that single declaration KosmoJS compiles schemas that run in **both** directions: ```ts export default defineRoute<"users/[id]", [ number, // validate id param as number [!code hl] ]>(({ PUT }) => [ PUT<{ json: { // [!code hl:4] name: string; email: VRefine }, }>( async (ctx) => { /* ... */ }, ), ]); ``` * **In the browser**, fetch client validates params and payload *before* the request leaves. Invalid data throws immediately - no round trip, and the same schemas are exposed as [validationSchemas](/fetch/validation#validation-schemas) for live form feedback. * **On the server**, the request is validated again on arrival - because a server that trusts its clients is not a server, it is a suggestion box. The two are the same compiled schema, so they cannot disagree, and neither can drift from the types the handler is written against. [Details ›](/validation/intro#end-to-end-validation) And under SSR, the client-side check is disabled automatically - validation runs on the API endpoint only. ## What this costs you An honest list, because the trade is real: * **You write a route file.** Not a function in a component - a file under `api/`, with a URL. * **No closure capture across the boundary.** A page cannot reach into a server-side variable simply because it is in scope; it calls a route, and the route reads the database. This is the constraint that makes the boundary real. * **No progressive-enhancement form posts**, no `useActionState` equivalent - form state is your framework's job, with the client's validation schemas for field errors. And what it buys: an API a second client can use, a boundary your middleware actually controls, URLs in your logs, a spec you can publish, folders you can deploy separately - and, during SSR, none of the latency that usually comes with any of it. ## The déjà-vu, resolved The 2000s page was fast to write and impossible to keep. The 2010s API was durable and chatty to build against. They were solving different problems, and neither one was actually wrong about its own: * colocation is about the cost of crossing a boundary * separation is about whether the boundary exists Those are independent questions, and answering them independently is the whole of the design here. Keep the API. Make crossing it free where it can be free. Ask again in ten years. *** * [Migration Tips](/essentials/migration-tips) * [Isomorphic fetch](/fetch/isomorphic-clients) * [Validation](/validation/intro) --- --- url: /essentials/why-codegen.md description: >- Two very different things share the word "codegen". KosmoJS only does the safe kind - derivation into a git-ignored lib/ folder, plus boilerplate seeded into blank files and never touched again. --- For many of us, "codegen" is a good reason to close the tab. Most code generation earned that. But two very different things share that one word. ## First, What Is What **Consider a tool that writes a file, lets you edit it, then overwrites your edit.** A perfect example of a "codegen" imperfection - the dark side. Rails scaffolds, SOAP/WSDL stubs, Interface Builder, checked-in protoc output, Swagger client generators - the story is always the same: you edited the output, something regenerated it, your work vanished. So you stopped regenerating, and maintained a second source of truth by hand forever. Call that **scaffolding** - written once, becomes yours, drifts. **Now consider another tool. It reads your code and derives things you never edit.** A perfect example of a compiler at work - the light side. * `tsc` turns your TypeScript into JavaScript * The JSX transform turns `
` into function calls * Vite turns your modules into a bundle * Prisma turns a schema into a typed client Nobody calls these a smell, and nobody edits their output. Call that **derivation** - recomputed from a single input, never edited, disposable. ## What KosmoJS Does ### It derives Your routes are the input. Routing trees, typed fetch clients, validation schemas, the OpenAPI spec - all output, all landing in `lib/`, which is git-ignored. Not a second copy of your code; a derivative of it. Vite and friends do the same thing and hide the result in `node_modules`. KosmoJS keeps it in `lib/` deliberately: when something looks wrong, the derived code is right there in your editor, readable, with your own route names on it. ### It seeds A newly created route file gets its boilerplate, so you are not bootstrapping every route by hand. > Only blank files are seeded. > The moment a file has content - boilerplate or your own edit - it is never touched again. > Not by a re-seed, not by a template change, not by a version bump. And if the boilerplate isn't what you want, adapt it or switch it off entirely - see [backend](/backend/custom-templates) and [frontend](/frontend/custom-templates) templates. ### That's All It doesn't scaffold in user space. It reads your routes, wires them into the native routers, and derives assets you can import where you find them useful. KosmoJS owns the `lib/` folder, you own `src/` and everything else. *** The objection to codegen is really an objection to scaffolding. Derivation is just compilation, and you already trust several layers of it. Every project has derived artifacts. The only question is whether a machine derives them on every build, or a person derives them by hand on a schedule they will eventually forget. --- --- url: /cli/intro.md description: >- Every KosmoJS command - create kosmo and kosmo folder for scaffolding, serve, preview, build and typecheck for everything after, plus the interactive and flag-driven modes and every message a command exits with. --- KosmoJS ships two binaries. **create-kosmo** bootstraps a project - you run it once, through `npm create kosmo`. **kosmo** does everything after that. It comes with `@kosmojs/cli`, a devDependency of every project, and `package.json` wires it to scripts so you rarely type the binary name: | Script | Command | What it does | |---|---|---| | `pnpm dev` | `kosmo serve` | Dev server for every source folder | | `pnpm preview` | `kosmo preview` | Production build, served and rebuilt on change | | `pnpm build` | `kosmo build` | Production build | | `pnpm typecheck` | `kosmo typecheck` | `tsc --noEmit` per source folder, plus the project root | | `pnpm folder` | `kosmo folder` | Add a source folder to the project | All five run from the **project root** - the directory holding `package.json`. They read `distDir`, `devPort` and `previewPort` from it, and refuse to start if any is missing. `-h` / `--help` prints the full usage for either binary. ## The commands | Scaffolding | | |---|---| | [create kosmo](/cli/create) | Bootstrap a new project - run once | | [kosmo folder](/cli/folder) | Add a source folder to an existing project | | Running a project | | |---|---| | [kosmo serve](/cli/serve) | Dev server on `devPort`, Vite + HMR, always client-rendered | | [kosmo preview](/cli/preview) | Production build served on `previewPort`, rebuilt on change | | [kosmo build](/cli/build) | Production build only - `dist/run.js` plus a per-folder tree | | [kosmo typecheck](/cli/typecheck) | `tsc --noEmit` per source folder, plus the project root | ## Interactive vs CLI mode The two scaffolding commands - `npm create kosmo` and `kosmo folder` - each have an interactive flow and a flag-driven one. | Invocation | Mode | |---|---| | Any flag passed | CLI | | No flags, stdout is a terminal | Interactive | | No flags, stdout is **not** a terminal | CLI, with an empty flag set | There is no partial prompting. Once you are in CLI mode, anything you left out must have a default, or the command errors out. `-q` / `--quiet` suppresses CLI mode's output. Errors still print. ### The non-terminal case This is the one that surprises people, and it has nothing to do with how you typed the command. The prompts disappear whenever stdout is not a terminal: * piping the output - `pnpm folder admin | tee setup.log` * running it from a setup or CI script * running it in a container or an agent sandbox with no terminal attached So the same invocation that prompts in your shell becomes a CLI-mode run with no flags. It fails on the first required value rather than hanging on a question nobody can answer. That is deliberate: a scaffolder blocked on an invisible prompt is worse than one that tells you which flag is missing. ## When a command refuses to run | Message | Cause | |---|---| | package.json does not exist or some of distDir / devPort / previewPort is not set | Not in the project root, or some of listed key(s) are missing. | | Invalid command, use one of folder, serve, build, preview, typecheck | Typo, or a command from another framework's CLI. | | No source folders detected | No `src/*/kosmo.config.ts` anywhere. | | Some of the given names do not contain a valid KosmoJS source folder | A named folder doesn't exist or has no config. | | No folder name provided | `kosmo folder` in CLI mode with no name positional - including the non-TTY case. | | frontend is required: either provide `--frontend ` or `--no-frontend` flag | Neither half of the pair was passed. | | `--frontend` and `--no-frontend` are mutually exclusive; use only one | Both halves were. | | Target dir is not empty. Either remove dir contents or provide `--overwrite` flag | `create kosmo` in CLI mode, non-empty target. | | `./src/` already exists. Either remove it or provide `--overwrite` flag. | `kosmo folder` in CLI mode, folder taken. | --- --- url: /cli/create.md description: >- Bootstrapping a KosmoJS project with npm create kosmo - the interactive flow, the flags, and what the new project contains. --- ### When a command refuses to run - npm ```sh npm create kosmo demo ``` ### When a command refuses to run - pnpm ```sh pnpm create kosmo demo ``` ### When a command refuses to run - yarn ```sh yarn create kosmo demo ``` ### When a command refuses to run (cont.) The positional argument is the target directory, created if missing. Use `.` to bootstrap in the current directory: ```sh npm create kosmo . ``` A project name may contain alphanumerics and any of `. - + $ @`, may not start with a dash, and may not contain path traversal. ## The interactive flow If the target directory has anything in it other than `.git*`, `README*` or `LICENSE*`, you are asked first what to do with it - remove the existing files, keep them and overwrite as needed, or cancel. Then a handful of questions, all about the **first source folder**: 1. **Framework** - React, Vue, Solid, Svelte, MDX, or *None (API-only folder)* 2. **Backend Framework** - Hono, H3, Koa, or *None (client-only folder)* 3. **Enable server-side rendering (SSR)?** - skipped for MDX, where SSR is always on 4. **Enable static site generation (SSG)?** - asked only if SSR is enabled 5. **Enable TanStack Query?** - skipped for MDX, which does not support it You are not asked for the folder's name or base: the first folder is always **`app`**, with its pages at `/` and its API at `/api`. Add differently-shaped folders at any time with [kosmo folder](/cli/folder). ## CLI mode ```sh # npm needs -- to pass flags through npm create kosmo demo -- --frontend react --backend hono # pnpm and yarn do not pnpm create kosmo demo --frontend react --backend hono --ssr ``` | Flag | Meaning | |---|---| | `--frontend ` | `react`, `solid`, `vue`, `svelte`, `mdx`. | | `--no-frontend` | API-only folder - no `pages/`, no client entries. | | `--backend ` | `hono`, `h3`, `koa`. | | `--no-backend` | Client-only folder - no `api/` directory. | | `--ssr` | Enable [server-side rendering](/frontend/server-side-render). | | `--ssg` | Enable [static site generation](/frontend/static-site-generation). Implies SSR. | | `--tsq` | Enable [TanStack Query](/frontend/tanstack-query). Ignored on MDX folders. | | `--overwrite` | Proceed even though the target directory is not empty. | | `-q, --quiet` | Suppress output. | | `-h, --help` | Print usage and exit. | ::: warning The frontend and backend choices are never implied Either `--frontend ` **or** `--no-frontend` is required - omitting both is an error, and passing them both is an error too (same for backend). ::: ## What you get ```txt demo/ ├── package.json # type, distDir, devPort, previewPort, scripts, deps ├── .gitignore └── src/app/ ├── kosmo.config.ts # the frontend / backend blocks your answers imply ├── public/favicon.svg ├── api/index/index.ts # empty stub, if a backend was chosen ├── pages/index/index.tsx # empty stub, extension per framework └── entry/client.ts # empty stub, if a frontend was chosen ``` The stubs are **empty on purpose**. They are filled on the first `pnpm dev` or `pnpm build`, along with everything else the folder needs. `package.json` carries three project-level settings alongside the usual fields - [distDir, devPort, previewPort](/essentials/config#project-settings-package-json) - and the dependencies each chosen generator declares. *** ### After bootstrap `cd` into freshly created project (unless the project was bootstrapped in the current folder): ```sh cd ./demo ``` ### Install Dependencies #### Install Dependencies - npm ```sh npm install ``` #### Install Dependencies - pnpm ```sh pnpm install ``` #### Install Dependencies - yarn ```sh yarn install ``` ### Start the dev server The dev server completes the setup: it seeds the remaining project files and wires everything together. From then on it watches your routes and recomputes as you work: #### Start the dev server - npm ```sh npm run dev ``` #### Start the dev server - pnpm ```sh pnpm dev ``` #### Start the dev server - yarn ```sh yarn dev ``` #### Start the dev server (cont.) Your app is now running at `http://localhost:4556`. --- --- url: /cli/folder.md description: >- Adding a source folder to an existing project, interactively or from flags, and installing the dependencies a new folder needs. --- A project is a set of [source folders](/essentials/project-structure), and you can add one at any time. Each is a self-contained app with its own stack and its own URL prefixes - e.g. a marketing site at `/`, an admin app at `/admin`, an API-only service at `/svc`, etc. ```sh npm run folder # interactive # or `pnpm folder ` ``` **Folder Name** - becomes `src/`, and gives the folder its prefixes: * pages at `/` * API at `//api`. Edit `base` in `kosmo.config.ts` afterwards if you want different prefixes. If `src/` already exists, you are offered to remove / overwrite before proceed, or cancel. Interactive prompts you'll answer to: * **Frontend** and **Backend**, each with a *None* option * **SSR**, then **SSG** if SSR is on, then **TanStack Query** ## CLI mode The same flags as `create kosmo`, providing folder name as first argument. ```sh pnpm folder admin --frontend solid --backend h3 pnpm folder svc --no-frontend --backend hono pnpm folder docs --frontend mdx --no-backend --ssg ``` > Folder name is required, omit it and you get an error: `No folder name provided`. That gives `admin` its pages at `/admin` and its API at `/admin/api`, `svc` an API at `/svc/api` and no pages, `docs` pages at `/docs` and no API. Without `--overwrite`, an existing `src/` is an error rather than a prompt: ```txt ./src/admin already exists. Either remove it or provide --overwrite flag. ``` ## Install new dependencies A new folder usually brings new dependencies. The command diffs `package.json` before and after and prints only what was added: ```txt 💡 New dependencies added: solid-js, h3 📦 Install them before continue: $ npm install ``` Run the install before starting the dev server. The new folder's `kosmo.config.ts` already declares the stack, but the packages it writes imports for are not on disk yet. A dev server that was already running does not pick the folder up: folders are collected once, at startup. Restart it. ::: tip Changing the folder later `kosmo folder` writes `kosmo.config.ts` once; editing it afterwards is expected and supported. The config is read at startup, so restart the dev server after turning `ssr` on, adding a `backend`, or changing a `base`. [Configuration ›](/essentials/config#the-shape) ::: --- --- url: /cli/serve.md description: >- The development server - one process for the whole project, client modules through Vite with HMR, API routes hot-reloaded in the same process, on devPort. --- `serve` is the command you leave running while you work. One process covers every selected folder, and both halves of each: * client modules go through Vite, with HMR * API routes run in the same process, hot-reloaded on change * requests are dispatched between the two by the folder's `backend.base` No second command to start, no proxy to configure. ```sh pnpm dev # every folder pnpm dev admin front # just these two ``` Listens on **`devPort`** (default `4556`, configured in `package.json`). ## Dev is always client-rendered `serve` is Vite + HMR + client-side rendering, whether or not the folder has [SSR enabled](/frontend/server-side-render). Server rendering happens in the production build, so there is no server-rendered markup to look at here. This catches people out coming from Next, Nuxt or TanStack Start, where dev mirrors production rendering. To see anything server-rendered, use [kosmo preview](/cli/preview). ## Selecting folders With no arguments, every source folder is served. Name one or more to narrow the scope - the names are directory names under `src/`. A name with no `src//kosmo.config.ts` stops the command before anything runs, and a project with no folders at all reports `No source folders detected`. Narrowing changes what the one process serves: folders you left out are not mounted, so their paths 404 on `devPort` rather than falling through. ## Ports `devPort` is the only port you address. Each folder's Vite server and HMR socket get their own, picked from a free range derived from `devPort` - which is why the command insists on a `devPort` below `64000`, so that range still fits. `devPort` is not negotiated. If something else holds it, the dev server reports `Failed to start dev server on port 4556` and exits rather than moving, so the URL you have open never silently changes under you. [Development workflow ›](/dev-build-run/development-workflow) --- --- url: /cli/preview.md description: >- Building and serving the production output on previewPort, rebuilt on change - the only way to see SSR, bundling and the production validation policy locally. --- `preview` is the dev loop run against production output. The dev server never builds: it serves modules through Vite, client-rendered, with HMR. That makes it fast, and it also means a whole class of things simply does not exist there - server-rendered markup, hashed assets, chunk splitting, tree-shaking, the production validation policy. None of them are visible until you build, which is the gap `preview` closes. It builds the selected folders, runs [dist/run.js](/dev-build-run/building-for-production#one-entry-point-for-the-whole-project) - the same entry point production starts - then watches your sources and rebuilds on change, restarting the runner. ```sh pnpm preview # every folder pnpm preview front # just this one ``` Listens on **`previewPort`** (default `4558`, configured in `package.json`). That is deliberately not `devPort`: preview and the dev server run side by side, so you can compare client-rendered and server-rendered output in adjacent tabs. ## Selecting folders With no arguments, every source folder is built and served. Name one or more to narrow the scope - the names are directory names under `src/`. A name with no `src//kosmo.config.ts` stops the command before anything runs, and a project with no folders at all reports `No source folders detected`. Folders you leave out keep whatever is already in `distDir`, so the runner still serves them - built earlier, and not rebuilt when their sources change. ## Rebuild, not HMR A change triggers a full production rebuild, so expect seconds rather than the milliseconds of HMR. There is no module patching and no preserved state, because a production bundle has no running module graph to patch - and a preview you cannot trust is worse than none. A failed rebuild leaves the previous build serving, so a typo mid-edit prints an error to the terminal without taking the page down. Iterate on the dev server; reach for `preview` to verify. [Production preview ›](/dev-build-run/production-preview) --- --- url: /cli/build.md description: >- The production build - what it emits per folder, how partial builds stay safe, and the manifest dist/run.js reads at startup. --- ```sh pnpm build # every folder pnpm build front # just this one ``` The same build `preview` runs, minus the server and the watcher. Output goes to the `distDir` set in `package.json` - `dist/` by default. With no arguments, every source folder is built. Name one or more to narrow the scope - the names are directory names under `src/`. A name with no `src//kosmo.config.ts` stops the command before anything is written, and a project with no folders at all reports `No source folders detected`. ## What one folder's build does Each selected folder is built in full, in order: 1. **Resolve routes.** Every route file is read and its types resolved - this is what produces the validation schemas and the fetch client signatures. 2. **Run each generator's build step**, in the [fixed order](/essentials/config#bringing-your-own-generator). 3. **Bundle the client**, if the folder has a `frontend` - into `dist//client`, with `base` set to the folder's `frontend.base` and a Vite manifest alongside it. 4. **Bundle the backend**, if the folder has a `backend` - into `dist//api`, as ESM with sourcemaps. Two entry points: `api/app.ts` and `api/server.ts`. 5. **Run each generator's post-build step.** This is where the SSR/SSG bundles produced, if either enabled. Then the folder's manifest is written, and once every folder is done, `dist/run.js` is deployed. [Details ›](/dev-build-run/building-for-production#build-output) ## Partial builds Building a subset leaves the other folders' output untouched, so `pnpm build app` never invalidates `admin`. Within a folder, though, the build is not incremental: the client and backend output directories are emptied before they are rewritten. A folder is always rebuilt whole, never patched. ## The folder manifest Each build writes `dist//kosmo.json`, describing the folder in the terms `dist/run.js` needs to route to it: ```json [dist/front/kosmo.json] { "name": "app", "frontend": { "base": "/" }, "backend": { "base": "/api", "aliasPatterns": [] }, "ssr": false } ``` `frontend` and `backend` are present only when the folder has that half. `dist/run.js` reads these at startup to discover which folders exist and what each one claims. That indirection is what makes partial builds safe: the runner holds no folder-specific data, so it is rewritten identically on every build, and a folder built an hour ago is still served by a runner started after a one-folder rebuild. [Details ›](/dev-build-run/building-for-production#one-entry-point-for-the-whole-project) ## Notes The Vite cache is keyed by command - `var/.vite//build/` - so building never invalidates the dev server's cache, and `preview` shares the build's cache rather than warming a third one. The build does not typecheck. `tsc` is a separate step - [kosmo typecheck](/cli/typecheck) - so a type error does not stop a bundle from being emitted. What you deploy depends on how each folder renders. [Details ›](/dev-build-run/building-for-production#what-to-deploy) --- --- url: /cli/typecheck.md description: >- Typechecking each source folder against its own tsconfig, plus the project root run and what its include list decides. --- `typecheck` is the type safety the rest of KosmoJS leans on, run on demand. ```sh pnpm typecheck # every folder, plus the project root pnpm typecheck admin # just this folder pnpm typecheck . # project root only pnpm typecheck . admin # project root and admin ``` Typecheck is a manual run. The dev server and the build do not typecheck before transpiling. So a stale `defineRoute<"users/[id]">`, left behind after renaming `api/users/`, is a compile error neither of them will mention - the bundle is emitted and the route 404s. This is the command that catches it, in CI or before you commit. :::details If you need it running automatically before `pnpm build` Add a `prebuild` script in `package.json`: ```json [package.json] { "scripts": { "prebuild": "pnpm typecheck" } } ``` ::: ## Under the hood It runs `tsc --project --noEmit` in sequence, using the `typescript` version installed in the project: one run per selected source folder, plus one for the project root. Every selected tsconfig is checked, even after one of them fails. A failing folder does not hide the state of the others, so one run tells you everything there is to fix rather than the first thing. The command exits `1` if any run reported errors, and `0` only when all of them passed - so `pnpm typecheck` works as a CI gate without any extra wiring. ### Why separate runs Each source folder has its own [tsconfig.json](/essentials/config#typescript-config) with its own path mappings. A single `tsc` over the whole project would resolve them against the wrong one, so the folders are checked one at a time instead. ## Selective typechecking Provide no arguments and every source folder is checked, along with the project root. ```sh # every folder, plus the root pnpm typecheck ``` Provide one or more names and only those are checked, project root ignored. ```sh # one folder, no root pnpm typecheck admin ``` > The command fails loudly if a name is not a folder in `src/` with a valid `kosmo.config.ts`. Provide `.` and only project root is checked, no source folders. ```sh # the root, no folders pnpm typecheck . ``` Provide `.` with folder name(s) and you get the root checked along with the provided names. ```sh # the root and admin folder pnpm typecheck . admin ``` --- --- 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. --- With 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. When you create a new route, you rarely write it from scratch - it is automatically seeded with the correct boilerplate. 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) ## Native Routing Under the Hood A fair question: with custom parameter syntax and `path-to-regexp` patterns, is KosmoJS running its own router at runtime? It isn't. `path-to-regexp` is used only at build time, to parse your directory structure into route definitions. At runtime, those parsed routes are registered with each framework's **native router** - exactly as you would register them by hand. Nothing sits between a request and your framework's matching logic. ``` build time runtime ────────── ─────── directory structure native router registration api/users/[id]/index.ts -> Hono/H3/Koa router pages/users/[id]/index.tsx -> React/Solid/Vue router │ │ └── parsed via path-to-regexp ──────────┘ ``` The payoff is that you keep the full, native routing of whatever framework you chose - nothing is wrapped, shimmed, or reimplemented: * **Backend** - Hono/H3/Koa router handles your routes natively. * **Frontend** - React Router, Solid Router, and Vue Router each receive standard route definitions, so nested layouts, lazy loading, loaders/preloads, and navigation guards all behave exactly as documented by those frameworks. **KosmoJS is the chassis, not the engine.** It gives every source folder the same consistent, directory-based way to define routes; the engine doing the actual routing is the framework you picked. --- --- 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/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. --- Parameter types supported out of the box, with same syntax for both API 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 support varies by framework: * **Hono/H3** - partial support, with caveats * **Koa** - full support * **Vue, Svelte and MDX** - full support * **React Router** - `.ext` suffix only * **SolidJS Router** - not supported Prefer simple segments for frontend routes. [Full support matrix ›](/essentials/frameworks#routing-syntax-support) ## 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 is 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 ``` ::: info Limited support across frameworks Power Syntax fully works with Koa routes only. Hono will match that routes but params will be registered using positional index + a hash, e.g. `_0abc` or `_1xyz`. H3 won't match any of these routes. For client pages use simple segments instead. ::: --- --- url: /backend/intro.md description: >- KosmoJS API layer supports Hono, H3 and Koa frameworks, with elegant middleware composition, end-to-end type safety, and flexible route definitions inspired by Sinatra framework. --- A source folder's API runs on one backend framework - **Hono**, **H3** or **Koa**. * [Hono](https://hono.dev) - exceptional performance, runs unchanged on Node, Deno, Bun and edge platforms. * [H3](https://h3.dev) - comparable performance and reach, built around Web standards. * [Koa](https://koajs.com) - battle-tested Node ecosystem, elegant async/await middleware. The decision matters less than it looks. Route layout, middleware composition, payload validation, fetch clients, all behave identically whichever you pick. What changes is the context object your handlers receive - reading a body, setting a response, raising an error stay your framework's own idioms, untouched. ## What's in `api/` Creating a source folder with a backend seeds a small, fixed set of files. Each is a real source file you own - they are written once, never re-seeded. ```text src//api/ ├── app.ts -> builds the backend app instance ├── server.ts -> standalone server entry ├── dev.ts -> dev-only hooks ├── errors.ts -> the central error handler ├── use.ts -> global middleware (every route in this folder) ├── env.d.ts -> global context/state types, custom slots │ └── users/ ── a route folder ── ├── use.ts -> middleware for /users and everything under it ├── index.ts -> the route -> /api/users ├── types.ts -> colocated helper, NOT a route └── [id]/ └── index.ts -> the route -> /api/users/:id ``` ### Foundation files | File | What it is | When you touch it | |---|---|---| | `app.ts` | Builds the app with `appFactory()`. The callback hands you the **native** Hono/H3/Koa instance - this is where the error handler is registered and where [app middleware](/backend/middleware#app-middleware) or any framework plugin goes. | Adding app middleware; enabling [debug](/dev-build-run/development-workflow#inspecting-api-routes) | | `server.ts` | The standalone entry that boots `app.ts` - what `node dist//api/server.js` runs in production. | Rarely | | `dev.ts` | Dev-only hooks: `requestHandler()` returns the handler the dev server dispatches to (override it for WebSockets or custom dispatch), and `teardownHandler()` runs **before every reload** - close DB connections and sockets here or they leak across restarts. | WebSockets; connection cleanup | | `errors.ts` | The central error handler, registered by `app.ts`. The default distinguishes `ValidationError` and `HTTPError`, then content-negotiates JSON or plain text. | Customizing error responses | | `use.ts` | [Global middleware](/backend/middleware) - runs for every route in this folder. | Request id, auth, permission checks | | `env.d.ts` | Module augmentation for folder-wide types: `DefaultVariables`/`DefaultBindings` (Hono), `DefaultContext` (H3), `DefaultState`/`DefaultContext` (Koa), plus [custom slot names](/backend/middleware#slot-composition). | Typing `ctx.state` / bindings | Only `app.ts` differs between backends, and only in how the error handler attaches: #### Foundation files - Hono ```ts // Hono: api/app.ts export default appFactory(routes, ({ app }) => { app.onError(defaultErrorHandler); }); ``` #### Foundation files - H3 ```ts // H3: api/app.ts export default appFactory(routes, ({ app }) => { app.use(onError(defaultErrorHandler)); }); ``` #### Foundation files - Koa ```ts // Koa: api/app.ts export default appFactory(routes, ({ app }) => { app.use(defaultErrorHandler); }); ``` ### Inside a route folder | File | What it is | |---|---| | `index.ts` | **The route.** Default-exports `defineRoute(...)`; its folder path becomes the URL. | | `use.ts` | [Cascading middleware](/backend/cascading-middleware) for this folder and everything beneath it. Exports `UseT` to extend the typed context downward. | | anything else | A colocated helper - schemas, types, queries, tests. Never a route, never scanned. | `index.ts` and `use.ts` are the only two filenames the backend watcher acts on. That is the whole convention: **one URL per folder, one file that defines it.** Derived code - validators, the route table, fetch clients, the OpenAPI spec - never lands here. It lives in `lib/`, is git-ignored, and you neither read nor edit it. [Details ›](/essentials/why-codegen) ## 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<"users">(({ GET, POST, PUT, DELETE }) => [ GET(async (ctx) => { /* retrieve */ }), POST(async (ctx) => { /* create */ }), PUT(async (ctx) => { /* update */ }), DELETE(async (ctx) => { /* delete */ }), ]); ``` This method-based routing style draws inspiration from [Sinatra](https://sinatrarb.com/) - the Ruby framework that pioneered it back in 2007. 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`. ::: tip HEAD is served by your GET handler `HEAD` is the one exception to the 405 rule. A route that defines `GET` but not `HEAD` still answers HEAD requests: they are dispatched to the `GET` handler and validated against its schemas, with the body dropped as the HTTP spec requires. Define `HEAD` explicitly only when you want to override that. Hono is the exception: its router ignores any `HEAD` handler you define, so the fallback to `GET` always wins there. ::: ## The Route Name Type Argument `defineRoute<"users/[id]">` restates the path the file already lives at, which looks redundant. It isn't - and it's worth one minute to understand why, because it explains a lot of what follows. The routing itself never needs it. The URL comes from the file's location, full stop. The string is there for **TypeScript**, which cannot see the file system. Everything KosmoJS knows about a route is placed into a `RouteMap` in `lib/`, keyed by route name: ```ts [lib/front/@api/routes.ts] export type RouteMap = { "users/[id]": { paramsDefaults: [string], // params, in path order paramsMappings: { id: 0 }, // name -> position cascadingState: UseT_apiUse & UseT_apiUsersUse, // merged use.ts context }, // ...every other route }; ``` The name is how the handler looks itself up in that map. That single lookup is what gives you: * **`ctx.validated.params` typed** - which params exist, in what order, refined to what. * **Cascading context typed** - the merged `UseT` of every `use.ts` above this route, which is why `ctx.get("user")` is typed without importing anything. [Details ›](/backend/cascading-middleware#type-safe-context-extension) * **The params refinement tuple checked against the real params** - a tuple longer than the route has parameters is a compile error. Because there is no runtime argument carrying it, TypeScript has nothing to infer it from - so the type argument is **required**, and you write it once when the file is created. In practice, you don't: the seeded [boilerplate](/backend/custom-templates) already contains the correct name. ::: tip What if it's wrong? It can't silently drift. `defineRoute` is constrained as `R extends keyof RouteMap`, so a name that doesn't match a real route is a **compile error**, not a runtime surprise. Rename `api/users/` to `api/people/` and the stale `defineRoute<"users/[id]">` fails to typecheck immediately - the same refactor-as-a-checklist property the typed [Link](/frontend/link-navigation) component gives you on the frontend. ::: The name is the route path **relative to `api/`**, without the trailing `index.ts` - so `api/users/[id]/index.ts` is `"users/[id]"`, and `api/index/index.ts` is `"index"`. Pages have no equivalent: a page component is an ordinary default export, and its routing is resolved by the framework's own router, so there is nothing to look up. ## Where routes end up: `frontend.base` and `backend.base` Each side of a source folder declares the URL prefix it owns, and each one is a **full path**. The two are resolved independently: ``` API route URL = join(backend.base, routeName) page URL = join(frontend.base, pagePath) ``` That is the one thing to hold on to; everything in the table follows from it. | `frontend.base` | `backend.base` | API routes live at | pages live at | Note | | --- | --- | --- | --- | --- | | `/` | `/api` | `/api/` | `/` | app at the root | | `/admin` | `/admin/api` | `/admin/api/` | `/admin/` | the API nested under the folder | | `/admin` | `/api/v2` | `/api/v2/` | `/admin/` | nothing requires them to nest | | none | `/api` | `/api/` | none | an API-only folder | | `/docs` | none | none | `/docs/` | a pages-only folder | Nesting the API under the pages prefix is a convention, not a rule - useful when a folder should be relocatable as a unit, since moving `frontend.base` means moving `backend.base` with it. ### An API-only folder Omit `frontend` entirely and give the backend the prefix it should own: ```ts // src/api/kosmo.config.ts export default defineConfig({ backend: { stack: "hono", base: "/api", // [!code hl] }, validation: true, }); // src/api/api/emails/index.ts -> /api/emails ``` The `api/` dir on disk never appears in the URL - it only separates server routes from `pages/`. ### How requests are dispatched The dev server and the built `dist/run.js` route by prefix: more specific prefixes win, and a folder's `backend.base` is ranked ahead of its `frontend.base` - so `/front/api/users` reaches the API even though `/front` also matches. A folder at `base: "/"` catches only what no other folder claims. The server prints the prefix table on start; read it when a route lands somewhere unexpected: ``` /admin/api -> admin /api/ -> api /webhooks/ -> webhooks /admin -> admin / -> docs ``` ## Type Safety Parameters, payloads, and responses are all typed through 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: /backend/context.md description: >- Learn about KosmoJS's enhanced context - unified bodyparser and metaparser APIs, and ctx.validated for type-safe validated data access --- KosmoJS extends the standard Hono/H3/Koa context with three additions: unified `bodyparser` and `metaparser` APIs, 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`. ## Unified Metaparser `ctx.metaparser` does the same for request metadata: ```ts ctx.metaparser.params() // route params, normalized ctx.metaparser.query() // query parameters, normalized ctx.metaparser.headers() // headers, as a plain object ctx.metaparser.cookies() // parsed cookies ``` These are synchronous - there is nothing to await - and cached the same way. `params()` and `query()` hand back normalized values rather than raw strings: splat params are split into arrays, and values are coerced to match your declared types - numbers for params, numbers and booleans for query. `headers()` and `cookies()` are plain parses. *** Both `ctx.metaparser` and `ctx.bodyparser` are rarely used in handlers directly. They are most useful in [edge middleware](/backend/edge-middleware) and in a [custom validator](/backend/middleware#overriding-validation). where `ctx.validated.*` is not filled yet. They are not there in [`api/app.ts`](/backend/middleware#app-middleware), which runs before the context is extended. ## Validated Data Access `ctx.validated` holds the validated, typed result for each target you defined: ```ts export default defineRoute<"users">(({ 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; }), ]); ``` `ctx.metaparser.params()` gives you the same normalized values before validation runs - useful in [edge middleware](/backend/edge-middleware), where `ctx.validated` is still empty. The underlying raw params still exist if you need them: * Hono - `ctx.req.param()` * H3 - `event.context.params` * Koa - `ctx.params` --- --- 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, // number action, // UserAction | undefined } = ctx.validated.params; }), ]); ``` Positions are optional - but to refine the second param you must also provide the first. ### ❗ Keep the Tuple Brackets Literal The refinement tuple's `[]` must be written inline. Type aliases used *inside* it are fine; extracting the whole tuple to a named type is not: ```ts // ✅ works - brackets literal, contents aliased defineRoute<"[id]/[action]", [UserID, UserAction]> // ❌ won't work - the brackets themselves are behind an alias type Params = [UserID, UserAction]; defineRoute<"[id]/[action]", Params> ``` The position is read structurally from the source, mapping each slot to a route parameter, so an alias leaves nothing to destructure. It fails silently: the schema does not build and **every** request is rejected. The same rule covers the `response` tuple and the `VRefine` constraint object. [The bracket rule ›](/validation/refine#keep-the-wrapping-brackets-literal) Refinements also drive runtime validation - invalid params are rejected before your handler runs. [Details ›](/validation/params) ## Typing Payload and 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 }); // return ctx.json(user); // Hono // return user // H3 // ctx.body = user; // Koa }), ]); ``` Both payload and response are validated at runtime, not just at compile time. [Details ›](/validation/payload) ## Typing State & Context `defineRoute` accepts four type arguments: ### Typing State & Context - Hono ```ts // Hono: api/users/[id]/index.ts defineRoute< "route-name", ParamsTuple, // param refinements Variables, // route-specific locals Bindings, // route-specific bindings > ``` ### Typing State & Context - H3 ```ts // H3: api/users/[id]/index.ts defineRoute< "route-name", ParamsTuple, // param refinements Context, // route-specific context properties > ``` ### Typing State & Context - Koa ```ts // Koa: api/users/[id]/index.ts defineRoute< "route-name", ParamsTuple, // param refinements State, // route-specific state/locals Context, // route-specific context properties > ``` ### Typing State & Context (cont.) Use the third and fourth arguments for types that are unique to a specific route: ### Typing State & Context - Hono ```ts // Hono: api/users/[id]/index.ts 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; }), ]); ``` ### Typing State & Context - H3 ```ts // H3: api/users/[id]/index.ts export default defineRoute< "users/[id]", [number], { permissions: Array<"read" | "write"> }, // event.context.permissions >(({ GET }) => [ GET(async (event) => { const { id } = event.validated.params; const { permissions } = event.context; }), ]); ``` ### Typing State & Context - Koa ```ts // Koa: api/users/[id]/index.ts 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; }), ]); ``` ### Typing State & Context (cont.) If you find yourself declaring the same properties across many routes, move them to the global declarations in `api/env.d.ts` instead - a `declare module "_/api"` augmentation rather than a `defineRoute` type argument. ## 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: ### Global Context Types - `api/env.d.ts` - Hono ```ts // Hono: api/env.d.ts export declare module "_/api" { interface DefaultVariables { permissions: Array<"read" | "write" | "admin">; } interface DefaultBindings { DB: D1Database; } } ``` ### Global Context Types - `api/env.d.ts` - H3 ```ts // H3: api/env.d.ts export declare module "_/api" { interface DefaultContext { permissions: Array<"read" | "write" | "admin">; } } ``` ### Global Context Types - `api/env.d.ts` - Koa ```ts // Koa: api/env.d.ts export declare module "_/api" { interface DefaultState { permissions: Array<"read" | "write" | "admin">; } interface DefaultContext { authorizedUser: User; } } ``` ### Global Context Types - `api/env.d.ts` (cont.) > **Important:** declaring types in `env.d.ts` doesn't set the values - > you still need the middleware that actually populates them. The right place to set global properties is `api/use.ts` file. It runs for every endpoint, so properties becomes available for all routes: ### Global Context Types - `api/env.d.ts` - Hono ```ts // Hono: api/env.d.ts import { use } from "_/api"; export default [ use(async (ctx, next) => { ctx.set("permissions", await getPermissions(ctx)); // [!code hl] return next(); }), ]; ``` ### Global Context Types - `api/env.d.ts` - H3 ```ts // H3: api/env.d.ts import { use } from "_/api"; export default [ use(async (event, next) => { event.context.permissions = await getPermissions(ctx); // [!code hl] return next(); }), ]; ``` ### Global Context Types - `api/env.d.ts` - Koa ```ts // Koa: api/env.d.ts import { use } from "_/api"; export default [ use(async (ctx, next) => { ctx.state.permissions = await getPermissions(ctx); // [!code hl] return next(); }), ]; ``` ### Global Context Types - `api/env.d.ts` (cont.) --- --- url: /backend/middleware.md description: >- Middleware chains and the Hono/H3/Koa onion model. Global middleware in api/use.ts that runs for every route in a source folder, method restrictions, and overriding defaults through slots. --- 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. The same API applies identically to all frameworks, only the middleware internals slightly differs by framework: ```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"); // ... }), ]); ``` When a POST request arrives, the execution order is like: ``` First middleware Second middleware POST handler Second middleware after next First middleware after next ``` > **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<"example">(({ use, GET, POST }) => [ use(firstMiddleware), GET(async (ctx) => { /* ... */ }), POST(async (ctx) => { /* ... */ }), use(secondMiddleware), // still runs BEFORE handlers [!code hl] ]); ``` ## Global Middleware Wiring same middleware into every route is tedious and dangerous. Use **global middleware** instead - add middleware to `api/use.ts` and it runs for **every route** - no imports, no registration, nothing to wire: ### Global Middleware - Hono ```ts // Hono: api/use.ts import { use } from "_/api"; export default [ // will run on every route use(async function requestId(ctx, next) { ctx.set("requestId", crypto.randomUUID()); return next(); }), ]; ``` ### Global Middleware - H3 ```ts // H3: api/use.ts import { use } from "_/api"; export default [ // will run on every route use(async function requestId(event, next) { event.context.requestId = crypto.randomUUID(); return next(); }), ]; ``` ### Global Middleware - Koa ```ts // Koa: api/use.ts import { use } from "_/api"; export default [ // will run on every route use(async function requestId(ctx, next) { ctx.state.requestId = crypto.randomUUID(); return next(); }), ]; ``` ### Global Middleware (cont.) This is the place for work that belongs to **routes**: loading the current user onto the context, permission checks, audit logging of writes - things that need a route to exist, and that want the request already validated. Anything narrower belongs in a [cascading use.ts](/backend/cascading-middleware) for a subtree, or in the route's own `use`. ::: warning `api/use.ts` is skipped for non-route responses A preflight `OPTIONS`, or a `405` for a method the route doesn't implement, is answered before any route chain runs - so global middleware never sees it. Which makes this the wrong home for **CORS**: the preflight would never reach your middleware, and the browser would reject the request before it ever sent the real one. CORS - and anything else that must appear on *every* response - belongs to [app middleware](#app-middleware). ::: ## Method-Specific Middleware Use the `on` option to restrict middleware to specific HTTP methods: ### Method-Specific Middleware - Hono ```ts // Hono: api/example/index.ts export default defineRoute<"example">(({ GET, POST, use }) => [ use(async (ctx, next) => { ctx.set("user", await verifyToken(ctx.req.header("authorization"))); return next(); }, { on: ["POST"], // [!code hl] }), GET(async (ctx) => { // no auth required }), POST(async (ctx) => { // ctx.get("user") is available }), ]); ``` ### Method-Specific Middleware - H3 ```ts // H3: api/example/index.ts export default defineRoute<"example">(({ GET, POST, use }) => [ use(async (event, next) => { event.context.user = await verifyToken(event.req.headers.get("authorization")); return next(); }, { on: ["POST"], // [!code hl] }), GET(async (event) => { // no auth required }), POST(async (event) => { // event.context.user is available }), ]); ``` ### Method-Specific Middleware - Koa ```ts // Koa: api/example/index.ts export default defineRoute<"example">(({ GET, POST, use }) => [ use(async (ctx, next) => { ctx.state.user = await verifyToken(ctx.headers.authorization); return next(); }, { on: ["POST"], // [!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 logger 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) => { /* ... */ }), ]); ``` > When overriding via slot, explicitly set [on](#method-specific-middleware) option 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 "_/api" { // ... interface UseSlots { logger: string; // [!code hl] } } ``` Then use it anywhere: ```ts use(async (ctx, next) => { /* ... */ }, { slot: "logger" }) ``` Some slot names are reserved and already positioned in the chain - the [`edge:`](/backend/edge-middleware) family is the one you are likely to reach for. *** ### Promoting to the Route Edge Any `use()` entry claiming an `edge:` prefixed slot in `api/use.ts`, in a cascading `use.ts`, or in the route itself - is lifted out of its usual position and run first in the matched route's chain instead, ahead of validation. That is [edge middleware](/backend/edge-middleware). ## Overriding Validation Validation is middleware too, and every target sits in a reserved slot - so any of them can be replaced exactly the way you replace a `logger`. Available validation slots, each reserved for a specific validation target: ``` validate:params validate:query validate:headers validate:cookies validate:json validate:form validate:raw validate:response ``` > These are reserved slots - no `UseSlots` declaration in `api/env.d.ts` needed. Claim one and your middleware runs **instead of** the built-in validator for that target, say an endpoint accepts a body no schema can describe: ```ts [api/import/index.ts] export default defineRoute<"import">(({ POST, use }) => [ use(async (ctx, next) => { // NDJSON - one JSON document per line const body = await ctx.bodyparser.raw(); // ... return next(); }, { slot: "validate:json", // [!code hl] }), POST(async (ctx) => { /* ... */ }), ]); ``` Everything else about slots still applies: declare it in `api/use.ts` to replace validation folder-wide, in a [cascading use.ts](/backend/cascading-middleware) for a subtree, or in the route itself for one endpoint. ### What You Take Over One target, and only that one. Overriding `validate:json` replaces the JSON validator and nothing else - `ctx.validated.params`, `.query`, `.headers` and `.cookies` are still filled in by their own validators, still before your handler runs. What you give up is that target's entry in `ctx.validated`. The built-in validator loads the data, checks it, and publishes the result; yours is only expected to check. So `ctx.validated.json` stays unset, and the handler reads the body itself. Which costs nothing, because the parsers are shared: ::: tip Parsers are lazy and cached `ctx.bodyparser.()` and `ctx.metaparser.()` each run at most once per request and return the cached result afterwards. Call them wherever you like - in your validator, in the handler, in both. The request stream is read once no matter how many times you ask for it. ::: So the handler above just asks again, and gets the body your validator already parsed: ```ts [api/import/index.ts] POST(async (ctx) => { const records = await ctx.bodyparser.raw() // ... }), ``` Overriding `validate:response` works the same way in reverse: your middleware decides what a valid response looks like, and the built-in check no longer runs. ## App Middleware The outermost layer, and the only one KosmoJS doesn't compose for you. `api/app.ts` hands you the `Hono` / `H3` / `Koa` instance itself, so anything the framework can do at app level, you do here - written exactly as that framework's own docs describe: ### App Middleware - Hono ```ts // Hono: api/app.ts import appFactory, { routes } from "_/api:factory"; import defaultErrorHandler from "./errors"; export default appFactory(routes, ({ app }) => { app.onError(defaultErrorHandler); app.use(async (c, next) => { const started = performance.now(); await next(); console.log([ c.req.method, c.req.path, performance.now() - started ]); }); }); ``` ### App Middleware - H3 ```ts // H3: api/app.ts import { onError } from "h3"; import appFactory, { routes } from "_/api:factory"; import defaultErrorHandler from "./errors"; export default appFactory(routes, ({ app }) => { app.use(onError(defaultErrorHandler)); app.use(async (event, next) => { const started = performance.now(); await next(); console.log([ event.req.method, event.url.pathname, performance.now() - started ]); }); }); ``` ### App Middleware - Koa ```ts // Koa: api/app.ts import appFactory, { routes } from "_/api:factory"; import defaultErrorHandler from "./errors"; export default appFactory(routes, ({ app }) => { app.use(defaultErrorHandler); app.use(async (ctx, next) => { const started = performance.now(); await next(); console.log([ ctx.method, ctx.path, performance.now() - started ]); }); }); ``` ### App Middleware (cont.) This layer runs **first, on every request**, whether or not a route matched - so it is the only place that can answer a 404, or see traffic for URLs your `api/` tree knows nothing about. It is also the bluntest layer. There are no slots here and nothing downstream can replace it, and KosmoJS doesn't compose it. It runs before any route chain, so the KosmoJS helpers aren't on the context yet: no `ctx.validated`, no `ctx.metaparser`, no `ctx.bodyparser`. Read the request through the framework's own API - or move the check into [edge middleware](/backend/edge-middleware), which runs inside the route's chain and has them. Request logging, CORS, tracing, rate limiting by IP: things that are true of the connection rather than of the route. --- --- 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 can't skip parent `use.ts` The seeded boilerplate when you create a `use.ts` in an `api/` subfolder: ```ts [api/users/use.ts] import { use } from "_/api"; export type UseT = {}; export default [ use(async (ctx, next) => { return next(); }) ]; ``` > Some editors load the seeded content immediately, others require a brief unfocus/refocus. Beside the default exported middleware, every `use.ts` **in an `api/` subfolder** exports the `UseT` type - even if empty. This type extends the context for all routes underneath, giving you automatic type safety for anything the middleware adds. The global `api/use.ts` is the exception: it may export `UseT`, but the export is ignored there - global middleware types come from `api/env.d.ts` instead. ## 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. `UseT` makes this work. Define what your middleware adds: ### Type-Safe Context Extension - Hono ```ts // Hono: api/users/use.ts import { use } from "_/api"; export type UseT = { user: { id: number; role: "admin" | "user" }; }; export default [ use(async (ctx, next) => { const token = ctx.req.header("authorization")?.replace("Bearer ", ""); // validate before adding to context - UseT promises this property exists if (!token) throw new HTTPException(401, { message: "Authentication required" }); ctx.set("user", await verifyToken(token)); return next(); }) ]; ``` ### Type-Safe Context Extension - H3 ```ts // H3: api/users/use.ts import { use } from "_/api"; export type UseT = { user: { id: number; role: "admin" | "user" }; }; export default [ use(async (event, next) => { const token = event.req.headers.get("authorization")?.replace("Bearer ", ""); // validate before adding to context - UseT promises this property exists if (!token) throw new HTTPError({ status: 401, message: "Authentication required" }); event.context.user = await verifyToken(token); return next(); }) ]; ``` ### Type-Safe Context Extension - Koa ```ts // Koa: api/users/use.ts import { use } from "_/api"; export type UseT = { user: { id: number; role: "admin" | "user" }; }; export default [ use(async (ctx, next) => { const token = ctx.headers.authorization?.replace("Bearer ", ""); // validate before adding to state - UseT promises this property exists ctx.assert(token, 401, "Authentication required"); ctx.state.user = await verifyToken(token); return next(); }) ]; ``` ### Type-Safe Context Extension (cont.) Now every route under `/api/admin` has `user` typed on the context automatically - no imports, no type arguments on `defineRoute`: ### Type-Safe Context Extension - Hono ```ts // Hono: api/users/use.ts export default defineRoute<"admin/dashboard">(({ GET }) => [ GET(async (ctx) => { const user = ctx.get("user"); // typed as { id: number; role: "admin" | "user" } }), ]); ``` ### Type-Safe Context Extension - H3 ```ts // H3: api/users/use.ts export default defineRoute<"admin/dashboard">(({ GET }) => [ GET(async (event) => { const { user } = event.context; // typed as { id: number; role: "admin" | "user" } }), ]); ``` ### Type-Safe Context Extension - Koa ```ts // Koa: api/users/use.ts export default defineRoute<"admin/dashboard">(({ GET }) => [ GET(async (ctx) => { const { user } = ctx.state; // typed as { id: number; role: "admin" | "user" } }), ]); ``` ### Type-Safe Context Extension (cont.) `UseT` is imported from each `use.ts` in the hierarchy and merged 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. > The global `api/use.ts` does not need to export `UseT`. > Even if it does, the export is ignored - global middleware operates on types defined in `api/env.d.ts`. > `UseT` is for `use.ts` files in `api/` subfolders only, where the types cascade alongside the middleware itself. **Tip:** inner `use.ts` files can import `UseT` from outer ones, extend it, and re-export - avoiding duplicate type definitions across the hierarchy: ```ts [api/admin/settings/use.ts] import type { UseT as ParentT } from "../use"; export type UseT = ParentT & { settingsAccess: "read" | "write"; }; ``` ## 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 to run only on specific request method(s): ```ts // api/users/use.ts import { use } from "_/api"; export type UseT = { user: { id: number; name: string }; }; export default [ use(async (ctx, next) => { // will run on ANY request method return next(); }), use( async (ctx, next) => { // will run only on POST }, { on: ["POST"] }, ), ]; ``` ## Common Use Cases Cascading middleware is where subtree-wide concerns belong: authentication, permission checks, audit logging, per-section rate limiting. KosmoJS imposes nothing here - `use` accepts your framework's own middleware signature, so **any Hono/H3/Koa middleware package works unchanged**. There is nothing KosmoJS-specific about the middleware itself. ### Third-party middleware Add middleware to `use.ts` and it will run on every route underneath: #### Third-party middleware - Hono ```ts // Hono: api/users/use.ts import { rateLimiter } from "hono-rate-limiter"; import { use } from "_/api"; export default [ use( rateLimiter({ windowMs: 15 * 60 * 1000, limit: 100, keyGenerator: (ctx) => ctx.req.header("x-forwarded-for") ?? "anonymous", }), ), ]; ``` #### Third-party middleware - H3 ```ts // H3: api/users/use.ts import { use } from "_/api"; export default [ use(async function noStore(event, next) { event.res.headers.set("cache-control", "no-store"); return next(); }), ]; ``` #### Third-party middleware - Koa ```ts // Koa: api/users/use.ts import ratelimit from "koa-ratelimit"; import { use } from "_/api"; const db = new Map(); export default [ use(ratelimit({ driver: "memory", db, duration: 15 * 60 * 1000, max: 100 })), ]; ``` #### Third-party middleware (cont.) ::: warning Not CORS, though A `use.ts` is composed into each route's chain, so it only runs once a route has matched. A preflight `OPTIONS` is answered before that, and never reaches it. CORS belongs in `api/app.ts`, as [app middleware](/backend/middleware#app-middleware). ::: `api/app.ts` takes a callback receiving the native app instance - not the array of `use()` calls above - so third-party middleware is registered exactly as that framework documents it: `cors` below is whatever CORS middleware you wire up - `hono/cors`, `@koa/cors`, or your own header-setting middleware. What differs per backend is only how the error handler attaches: #### Third-party middleware - Hono ```ts // Hono: api/app.ts import appFactory, { routes } from "_/api:factory"; import defaultErrorHandler from "./errors"; import { cors } from "./cors"; // [!code ++] export default appFactory(routes, ({ app }) => { app.onError(defaultErrorHandler); app.use(cors({ origin: "https://example.com" })); // [!code ++] }); ``` #### Third-party middleware - H3 ```ts // H3: api/app.ts import { onError } from "h3"; import appFactory, { routes } from "_/api:factory"; import defaultErrorHandler from "./errors"; import { cors } from "./cors"; // [!code ++] export default appFactory(routes, ({ app }) => { app.use(onError(defaultErrorHandler)); app.use(cors({ origin: "https://example.com" })); // [!code ++] }); ``` #### Third-party middleware - Koa ```ts // Koa: api/app.ts import appFactory, { routes } from "_/api:factory"; import defaultErrorHandler from "./errors"; import { cors } from "./cors"; // [!code ++] export default appFactory(routes, ({ app }) => { app.use(defaultErrorHandler); app.use(cors({ origin: "https://example.com" })); // [!code ++] }); ``` ### Authentication for a subtree Gate a whole section of the API by dropping a `use.ts` into its folder. Everything under `/api/admin` is now behind the check, and every route beneath it gets `user` typed on the context through [UseT](#type-safe-context-extension) - no imports, no type arguments: ```txt api/ ├── use.ts -> global: request id, auth, rate limit └── admin/ ├── use.ts -> auth: everything under /api/admin ├── index.ts └── users/ └── index.ts ``` ```ts [api/admin/use.ts] import { use } from "_/api"; export type UseT = { user: { id: number; role: "admin" | "user" }; }; export default [ use(async function requireAdmin(ctx, next) { // verify, then populate - UseT promises the property exists downstream return next(); }), ]; ``` There is no bundled auth solution and no NextAuth-style integration: you verify the token and populate the context yourself, the native way for your framework (`ctx.set("user", ...)` on Hono, `event.context.user = ...` on H3, `ctx.state.user = ...` on Koa). ### Choosing between `use.ts` and route-level `use` | | Use a cascading `use.ts` | Use an inline `use` | |---|---|---| | Scope | a folder and everything beneath it | one route file | | Wiring | automatic - no imports | explicit, inside `defineRoute` | | Context types | cascade via `UseT` | via `defineRoute` type arguments | | Good for | auth, audit logging, rate limiting | one-off concerns for a single endpoint | Keep cascading middleware **generic**. It runs for sibling routes too, so a param like `id` may be undefined there - see [Parameter Availability](#parameter-availability). Parameter-specific logic belongs in the route handler. --- --- url: /backend/edge-middleware.md description: >- The first middleware a matched route runs - any use() entry under an edge-prefixed slot, at any level, ahead of validation, so authentication answers 401 before a schema can answer 400. --- Edge middleware is the first thing a matched route runs - ahead of validation, ahead of your global, cascading and route middleware. ``` app middleware <- api/app.ts, every request, matched or not edge middleware <- slot: "edge:*", first in the route's chain validation <- params, query, headers, cookies, body global middleware <- api/use.ts cascading use.ts route use() handler ``` ::: tip The route's edge Edge middleware sits at the *route* edge: it goes first once a route has matched, and nothing reaches it otherwise. [App middleware](/backend/middleware#app-middleware) in `api/app.ts` is what sits at the request edge - it sees every request, matched or not. ::: ## Why It Exists Validation runs before your middleware - global, cascading and route alike. Usually that is what you want: your middleware gets a request already known to be well-formed. For authentication it is exactly wrong. An expired token on a request that also has a malformed body produces a `400 ValidationError`, and the caller goes hunting through their payload for a problem that isn't there. They should have got a `401`. **Solution: make your auth middleware run before validation by using an `edge:` prefixed slot:** ### Why It Exists - Hono ```ts // Hono: api/use.ts import { HTTPError } from "@kosmojs/core/errors"; import { use } from "_/api"; export default [ use(async (ctx, next) => { const token = ctx.req.header("authorization")?.replace("Bearer ", ""); if (!token) { throw new HTTPError([401, "Authentication required"]); } return next(); }, { slot: "edge:auth", // [!code hl] }), ]; ``` ### Why It Exists - H3 ```ts // H3: api/use.ts import { HTTPError } from "@kosmojs/core/errors"; import { use } from "_/api"; export default [ use(async (event, next) => { const token = event.req.headers.get("authorization")?.replace("Bearer ", ""); if (!token) { throw new HTTPError([401, "Authentication required"]); } return next(); }, { slot: "edge:auth", // [!code hl] }), ]; ``` ### Why It Exists - Koa ```ts // Koa: api/use.ts import { HTTPError } from "@kosmojs/core/errors"; import { use } from "_/api"; export default [ use(async (ctx, next) => { const token = ctx.headers.authorization?.replace("Bearer ", ""); if (!token) { throw new HTTPError([401, "Authentication required"]); } return next(); }, { slot: "edge:auth", // [!code hl] }), ]; ``` ### Why It Exists (cont.) Now an unauthenticated request is rejected before a single schema is consulted. It is still your middleware, written with `use()` like any other - only its position changes. It isn't only for auth. Use it for anything that must run before validation - a logger, a rate limiter, a tenant lookup. Declare it globally in `api/use.ts` and it covers every route. Declare it in a [cascading use.ts](/backend/cascading-middleware) and it covers a subtree. Declare it in a route and it runs for that route only. ## Specifics **Empty `ctx.validated.*`** Validation hasn't run, so nothing has been checked yet. Use [metaparser](/backend/context#unified-metaparser) / [bodyparser](/backend/context#unified-bodyparser) instead. They contain normalized data, exactly as the validation routines see it. Or rely on raw data directly from the framework. **It is still per route.** Running first doesn't make it [app middleware](/backend/middleware#app-middleware): a request matching no route never reaches it, and neither do preflights or `405`s. **It composes.** Because it is a slot, a declaration further down replaces one from above under the [usual rules](/backend/middleware#slot-composition), keeping the same position - and a route can declare an `edge:` slot of its own with nothing above it to replace: ```ts [api/webhooks/stripe/index.ts] export default defineRoute<"webhooks/stripe">(({ POST, use }) => [ use(async (ctx, next) => { // this endpoint authenticates by signature, not by bearer token await verifyStripeSignature(ctx); return next(); }, { slot: "edge:auth", // [!code hl] }), POST(async (ctx) => { /* ... */ }), ]); ``` The same works a level up: one `edge:auth` entry in `api/public/use.ts` replaces the global check for everything in that subtree. ## Name Your Slots Every `edge:` prefixed name is reserved, so none of them needs a `UseSlots` declaration in `api/env.d.ts`. Typos are still caught: `edge-auth` is not a slot - and neither is bare `edge` yours to take, per the warning below. Pick a name per concern and you get as many edge middlewares as you like - declared here in `api/use.ts`, but any level works: ```ts [api/use.ts] export default [ use(rateLimit, { slot: "edge:ratelimit" }), // [!code hl] use(authenticate, { slot: "edge:auth" }), // [!code hl] ]; ``` They run at the edge in the order declared, and each is its own slot - so a route can substitute one and leave the others in place. The Stripe webhook above replaces `edge:auth` with signature verification while `edge:ratelimit` keeps applying to it. ::: warning `edge` itself is reserved - always prefix The bare `edge` slot is not a spare name. It holds the built-in middleware that extends the context, and claiming it replaces that: no `ctx.metaparser`, no `ctx.bodyparser`, no `ctx.validated`, and every validator, middleware and handler downstream that expects them breaks at once. Name yours `edge:auth`, `edge:ratelimit`, anything prefixed. Override `edge` only if replacing context extension is exactly what you mean to do, and you know what has to go back in its place. ::: ## So, Where Do I Add My Auth? Depends on whether any route needs to opt out. **Every route authenticates the same way** - put it in `api/app.ts` as [app middleware](/backend/middleware#app-middleware). It is the simpler setup: one middleware, no slots, nothing downstream can replace it by accident. It also covers what a route chain never sees - unmatched URLs, preflights, `405`s. `api/app.ts` hands you the native app instance, so the check is registered the way that framework documents it - no `use()`, no slot, nothing imported from `_/api`: ### So, Where Do I Add My Auth? - Hono ```ts // Hono: api/app.ts import appFactory, { routes } from "_/api:factory"; import defaultErrorHandler from "./errors"; export default appFactory(routes, ({ app }) => { app.onError(defaultErrorHandler); app.use(async (ctx, next) => { const token = ctx.req.header("authorization")?.replace("Bearer ", ""); if (!token) return ctx.text("Authentication required", 401); await next(); }); }); ``` ### So, Where Do I Add My Auth? - H3 ```ts // H3: api/app.ts import { onError } from "h3"; import appFactory, { routes } from "_/api:factory"; import defaultErrorHandler from "./errors"; export default appFactory(routes, ({ app }) => { app.use(onError(defaultErrorHandler)); app.use(async (event, next) => { const token = event.req.headers.get("authorization")?.replace("Bearer ", ""); if (!token) return new Response("Authentication required", { status: 401 }); return next(); }); }); ``` ### So, Where Do I Add My Auth? - Koa ```ts // Koa: api/app.ts import appFactory, { routes } from "_/api:factory"; import defaultErrorHandler from "./errors"; export default appFactory(routes, ({ app }) => { app.use(defaultErrorHandler); app.use(async (ctx, next) => { const token = ctx.headers.authorization?.replace("Bearer ", ""); ctx.assert(token, 401, "Authentication required"); await next(); }); }); ``` ### So, Where Do I Add My Auth? (cont.) **Some routes authenticate differently** - a webhook verifying a signature, a public health check, an endpoint behind its own token - use an `edge:` slot, in `api/use.ts` or wherever the check belongs. You keep the 401-before-400 ordering, and any route or subtree can substitute its own check. That is a different file with a different shape - an array of `use()` calls, not a callback receiving the app instance: ```ts [api/use.ts] import { use } from "_/api"; import { authenticate } from "./auth"; export default [ use(authenticate, { slot: "edge:auth" }), ]; ``` ::: warning They don't stack A route claiming `edge:auth` replaces the `edge:auth` entry from `api/use.ts` - it does **not** replace anything in `api/app.ts`. If you put the base check in `api/app.ts` and add a route-level `edge:auth` override, they both run and the route authenticates twice, by two different rules. Pick either one. ::: If you're unsure, start in `api/app.ts`. Moving it to the slot later is mostly mechanical - the same handler, wrapped in `use()`. Moving the other way is not: every route-level `edge:*` override stops overriding anything and starts running in addition. --- --- url: /backend/error-handling.md description: >- Handle errors gracefully in KosmoJS with customizable error handlers for Hono, H3 and Koa. Learn about default error handling, route-level overrides, and framework differences. --- Error handling starts with `api/errors.ts` file, customize it at your needs: ## Default Error Handler ### Default Error Handler - Hono ```ts // Hono: api/errors.ts import { accepts } from "hono/accepts"; import { HTTPException } from "hono/http-exception"; import { ValidationError, HTTPError } from "@kosmojs/core/errors"; import { errorHandlerFactory } from "_/api:factory"; export default errorHandlerFactory(async (error, ctx) => { if (error instanceof HTTPException) { return error.getResponse(); } const [status, message] = Array.isArray(error) ? error : error instanceof HTTPError ? [error.status, error.message] : error instanceof ValidationError ? [400, `${error.target}: ${error.errorMessage}`] : [error.statusCode || 500, error.message]; 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); }); ``` ### Default Error Handler - H3 ```ts // H3: api/errors.ts import { ValidationError } from "@kosmojs/core/errors"; import { HTTPError } from "h3"; import { errorHandlerFactory } from "_/api:factory"; export default errorHandlerFactory(async (error, event) => { const [status, message = "Unknown error occurred"] = Array.isArray(error) ? error : error instanceof HTTPError ? [error.status, error.message] : error instanceof ValidationError ? [400, `${error.target}: ${error.errorMessage}`] : [error.statusCode || 500, error.message]; const accept = event.req.headers.get("accept"); return accept?.includes("application/json") ? new Response(JSON.stringify({ error: message }), { status, headers: { "Content-Type": "application/json" }, }) : new Response(message, { status, headers: { "Content-Type": "text/plain" }, }); }); ``` ### Default Error Handler - Koa ```ts // Koa: api/errors.ts import { HTTPError, ValidationError } from "@kosmojs/core/errors"; import { errorHandlerFactory } from "_/api:factory"; export default errorHandlerFactory(async (ctx, next) => { try { await next(); } catch (error: any) { const [status, message] = Array.isArray(error) ? error : error instanceof HTTPError ? [error.status, error.message] : error instanceof ValidationError ? [400, `${error.target}: ${error.errorMessage}`] : [error.statusCode || 500, error.message]; ctx.status = status; if (ctx.accepts("json")) { ctx.body = { error: message }; } else { ctx.body = message; } } }); ``` ### Default Error Handler (cont.) It's a regular file - customize it freely. It is then wired into `api/app.ts`: * *Hono*: `app.onError(defaultErrorHandler)` * *H3*: `app.use(onError(defaultErrorHandler))` * *Koa*: `app.use(defaultErrorHandler)` ## Key differences by framework | Framework | Details | |-----------|---------------| | **Hono** | `app.onError()` catches everything (`await next()` does **not** throw); returns a `Response`. Per‑route behavior by branching inside `app.onError()`. | | **H3** | `app.use(onError(errorHandler))` captures any thrown error; returns a `Response`, plain object or string. Branch inside `errorHandler` based on `event.url` or other properties. | | **Koa** | `defaultErrorHandler` is a middleware that wraps `await next()` in a `try`/`catch` and set `ctx.status`/`ctx.body` when errors thrown. | *** #### Summary * **Hono** - `onError` is the single entry point; you return a `Response`. * **H3** - `onError` behaves like Hono's: you return the response directly. * **Koa** - `await next()` throws. Errors captured in a middleware. ## Let Handlers Fail The whole point of a central handler is that route code doesn't have to think about error responses. So don't wrap handler logic in `try`/`catch` just to turn a failure into a response - throw, and let it propagate: ### Let Handlers Fail - Hono ```ts // Hono: api/users/[id]/index.ts import { HTTPError } from "@kosmojs/core/errors"; export default defineRoute<"users/[id]", [number]>(({ GET }) => [ GET(async (ctx) => { const { id } = ctx.validated.params; const user = await db.users.find(id); // ✅ throw - api/errors.ts turns it into a response if (!user) throw new HTTPError([404, "User not found"]); return ctx.json(user); }), ]); ``` ### Let Handlers Fail - H3 ```ts // H3: api/users/[id]/index.ts import { HTTPError } from "@kosmojs/core/errors"; export default defineRoute<"users/[id]", [number]>(({ GET }) => [ GET(async (event) => { const { id } = event.validated.params; const user = await db.users.find(id); // ✅ throw - api/errors.ts turns it into a response if (!user) throw new HTTPError([404, "User not found"]); return user; }), ]); ``` ### Let Handlers Fail - Koa ```ts // Koa: api/users/[id]/index.ts import { HTTPError } from "@kosmojs/core/errors"; export default defineRoute<"users/[id]", [number]>(({ GET }) => [ GET(async (ctx) => { const { id } = ctx.validated.params; const user = await db.users.find(id); // ✅ throw - api/errors.ts turns it into a response if (!user) throw new HTTPError([404, "User not found"]); ctx.body = user; }), ]); ``` ### Let Handlers Fail (cont.) > `HTTPError` takes a single `[status, message]` **tuple**, not two arguments. ```ts // ❌ don't - the error never reaches api/errors.ts, // the status is invented locally, and nothing gets logged centrally GET(async (ctx) => { try { // ... } catch (error) { // ... } }); ``` A per-route `catch` costs you three things: the response shape drifts from every other endpoint, whatever logging/reporting you added to `api/errors.ts` never sees the failure, and a `ValidationError` caught this way loses its structured `target`/`errors` detail. `throw` is also how you signal an *expected* failure. The seeded handler understands several shapes: * `HTTPError` (from `@kosmojs/core/errors`) - status plus message, in every framework. * Your framework's native error - `HTTPException` (Hono), `HTTPError` (H3), `ctx.throw()`/`ctx.assert()` (Koa). * A `[status, message]` tuple - the shorthand the seeded handler destructures first. * `ValidationError` - thrown for you by the validation layer; answered with a 400. * Anything else - `error.statusCode || 500`. **Catch inside a handler only when you intend to recover** - falling back to a cached value, retrying a flaky upstream, or converting a third-party error into a meaningful one. Then re-throw what you can't handle: ```ts try { // ... } catch (error) { if (error instanceof UpstreamTimeout) { // ... real recovery } throw error; // not ours to handle - let api/errors.ts decide } ``` The same rule holds for middleware: code after `await next()` runs on the way out, so wrapping `next()` in `try`/`catch` swallows errors for everything downstream. Put cross-cutting error concerns in `api/errors.ts` instead - it is a regular file you own, and it is the one place that sees every failure. --- --- url: /backend/aliases.md description: >- Serve an existing API route at an additional public URL - well-known paths, legacy URLs, and public-facing names that differ from the route on disk. --- Route names come from the filesystem, which is what makes them predictable. Occasionally a URL has to be something else: `/feed.xml` rather than `/rss`, or a path a previous system published that you cannot break. `alias` in the `backend` block serves an existing route at an additional URL: ```ts [kosmo.config.ts] export default defineConfig({ backend: { stack: "hono", base: "/api", alias: { "/feed.xml": "rss", "/members/[id]": "users/[id]", }, }, }); ``` The key is the URL to serve. The value is the name of the route that handles it. ## The key is absolute An alias URL is **not** prefixed by [backend.base](/essentials/config#backend-base-required). It is the whole path, from the root of the host: ```ts backend: { base: "/api", alias: { // served at /feed.xml, not /api/feed.xml "/feed.xml": "rss", }, } ``` That is the point of the feature - a well-known URL like `/feed.xml` or `/healthz` usually has to sit outside the API prefix, where a filesystem route could not put it. ## Dynamic segments must match by name When the alias carries parameters, the target route's parameters must all be present, with the same names and the same kind. Their **positions may differ**: ```ts alias: { // ok - same parameter, different surrounding path "/members/[id]": "users/[id]", // ok - same two parameters, reordered "/[org]/team/[id]": "orgs/[org]/members/[id]", // 404 - target expects "id", alias supplies "userId" "/members/[userId]": "users/[id]", // 404 - target expects a parameter the alias never supplies "/members": "users/[id]", } ``` A mismatch is not a startup error. The alias is registered and the request 404s, so check the shape when an alias silently fails to resolve. ## What the alias shares An alias is another entry pointing at the same route - not a copy of it, and not a redirect. No `3xx` is issued; the URL the client asked for is the URL it keeps. The handler is the same function, so everything attached to the route comes with it: its [middleware](/backend/middleware), the [cascading middleware](/backend/cascading-middleware) of the directories above it, and its [runtime validation](/validation/intro). There is nothing to duplicate and nothing that can drift between the two URLs. ## What it does not touch Aliases are a server-side routing concern only. The [fetch client](/fetch/intro) is keyed by the route's own name, so client code calls `fetchClients["users/[id]"]` whichever URL the route also answers on. The [OpenAPI spec](/openapi) documents the route once, under its route name. Reach for an alias when an outside consumer needs a particular URL. For an extra path your own frontend calls, a route file is simpler. ## Supported everywhere `hono`, `h3` and `koa` all accept `alias`, and the semantics above are identical on each - only the router pattern the alias compiles to differs. --- --- url: /backend/custom-templates.md description: >- Override the seeded defineRoute boilerplate for specific API routes using glob pattern matching. Seed consistent CRUD endpoints across many routes in Hono, H3 and Koa. --- Every backend generator accepts a `templates` option. When you create a new route file whose name matches one of your patterns, **your** boilerplate is written into it instead of the built-in placeholder. Templates seed route `index` files - `api/**/index.ts` - and nothing else. Every other seeded file gets the built-in minimal boilerplate to start from. ## Configuration Pass `templates` through the `backend` block in the source folder's `kosmo.config.ts`, keyed by route-name pattern: ```ts [kosmo.config.ts] import { defineConfig } from "@kosmojs/dev"; const crudTemplate = ` import { defineRoute } from "_/api"; export default defineRoute<"{{route.name}}">(({ GET, POST, PUT, DELETE }) => [ GET(async (ctx) => { /* list or read */ }), POST(async (ctx) => { /* create */ }), PUT(async (ctx) => { /* update */ }), DELETE(async (ctx) => { /* delete */ }), ]);`; export default defineConfig({ backend: { stack: "hono", base: "/api", templates: { "admin/**": crudTemplate, }, }, }); ``` Keys match the **route name** - the path relative to `api/`, without the trailing `index.ts`. So `api/admin/users/index.ts` is matched as `admin/users`. ## Pattern Syntax Patterns are [picomatch](https://github.com/micromatch/picomatch) globs matched against the route name. ### Single-depth wildcard (`*`) ```ts { "admin/*": template } ``` **Matches:** `admin/users`, `admin/settings`, `admin/[id]` **Excludes:** `admin/users/roles` (too deep), `admin` (too shallow) ### Multi-depth wildcard (`**`) ```ts { "admin/**": template } ``` **Matches:** `admin/users`, `admin/users/roles`, `admin/[id]/audit` - any depth. ### Exact match ```ts { "health": template } ``` ### Parameter segments are literal Route parameters are escaped before matching, so `[id]`, `{id}` and `{...path}` mean themselves rather than glob syntax: ```ts { "users/[id]": userTemplate, // required parameter "products/{category}": productTemplate, // optional parameter "docs/{...path}": docsTemplate, // splat parameter "shop/[category]/{sub}": shopTemplate, // combined } ``` ## Resolution Priority The **first matching pattern wins**, in the order the keys are written - so order them most specific first: ```ts templates: { "admin/audit": auditTemplate, // exact "admin/*": adminTemplate, // one level "**": fallbackTemplate, // everything else } ``` A route matching nothing gets the built-in placeholder. Set `"**"` to replace that default everywhere. ::: warning Numeric-looking patterns jump the queue JavaScript objects order integer-like keys first, regardless of where you wrote them - so a pattern such as `"2024/**"` is hoisted to the front and matches before anything above it. Prefix it with `./` to keep your written order: `"./2024/**"`. The `./` is stripped before matching. The same applies to [renderMode](/frontend/server-side-render#selecting-the-render-mode), which uses the same resolver. ::: ## Template Format A template is either a **string**, or a **function** of the resolved route returning a string - reach for the function form when the output depends on the route itself: ```ts templates: { // plain string, rendered with Handlebars "admin/**": crudTemplate, // or computed from the route "reports/**": (route) => ` import { defineRoute } from "_/api"; // endpoint: ${route.name} export default defineRoute<"${route.name}">(({ GET }) => [ GET(async (ctx) => { /* ... */ }), ]);`, } ``` String templates are rendered with Handlebars against a `{ route }` context, so a {{route.name}} placeholder interpolates the route name - which is what keeps the required [route-name type argument](/backend/intro#the-route-name-type-argument) correct in seeded files. ### Per-backend shapes Because the context API differs by backend, so does the boilerplate: #### Per-backend shapes - Hono ```ts // Hono: kosmo.config.ts const template = ` import { defineRoute } from "_/api"; export default defineRoute<"{{route.name}}">(({ GET }) => [ GET(async (ctx) => { // Always \`return\` the response return ctx.json({ ok: true }); }), ]);`; ``` #### Per-backend shapes - H3 ```ts // H3: kosmo.config.ts const template = ` import { defineRoute } from "_/api"; export default defineRoute<"{{route.name}}">(({ GET }) => [ GET(async (event) => { // return the value directly - objects serialize as JSON return { ok: true }; }), ]);`; ``` #### Per-backend shapes - Koa ```ts // Koa: kosmo.config.ts const template = ` import { defineRoute } from "_/api"; export default defineRoute<"{{route.name}}">(({ GET }) => [ GET(async (ctx) => { ctx.body = { ok: true }; }), ]);`; ``` #### Per-backend shapes (cont.) > Escape backticks (`` \` ``) and `${...}` inside template literals > that you do not want interpolated at config-evaluation time. ## Seeding CRUD Endpoints This is where route templates earn their place. Standing up endpoints for a dozen database tables means writing the same skeleton a dozen times - validation targets, method handlers, error shape, all identical apart from the type. Write it once: ```ts [kosmo.config.ts] const resourceTemplate = ` import { defineRoute } from "_/api"; import type { Resource, ResourcePayload } from "./types"; export default defineRoute<"{{route.name}}">(({ GET, POST }) => [ GET<{ query: { page?: number; limit?: number }, response: [200, "json", Array], }>(async (ctx) => { // list }), POST<{ json: ResourcePayload, response: [201, "json", Resource], }>(async (ctx) => { // create }), ]);`; export default defineConfig({ backend: { stack: "hono", base: "/api", templates: { "admin/**": resourceTemplate }, }, }); ``` Then create the route folders. Each new `index.ts` arrives with the right structure - typed handlers, declared `response` (so the [fetch client is typed](/fetch/type-safety#without-a-response-the-result-is-unknown) and OpenAPI picks it up), ready to adapt instead of retyped. Pair it with a colocated `types.ts` per route folder and the skeleton stays honest: the template references `./types`, and each route defines its own. ## Common Use Cases ```ts backend: { stack: "hono", base: "/api", templates: { "admin/**": crudTemplate, // consistent admin endpoints "webhooks/**": webhookTemplate, // signature verification + 200 fast "internal/**": internalTemplate, // runtimeValidation: false, trusted callers }, } ``` --- --- url: /frontend/intro.md description: >- Integrate KosmoJS directory-based routing with React, SolidJS, Vue, Svelte, or MDX. Automatic route configuration, type-safe navigation, and optimized lazy loading for modern frontend applications. --- Every source folder runs one frontend framework - **React**, **SolidJS**, **Vue**, **Svelte** or **MDX** - picked when you create the folder. Different folders can run different ones in the same project. Whichever you choose, the shape of the work is the same: components under `pages/` become the routes, navigation and data loading are typed end to end, and page code is split automatically. Nothing about the framework itself changes - you keep its router, its reactive model and its ecosystem, exactly as documented upstream. ## What's in the Folder These are the files that make the folder an application. Every one is a real source file you own: written once when the folder is created, never re-seeded behind your back. ```text src// ├── kosmo.config.ts -> this folder's config (and its Vite config) ├── tsconfig.json -> extends lib/front/tsconfig.json ├── index.html -> Vite's HTML entry ├── app.tsx -> global wrapper around EVERY route ├── router.ts -> wires routes into the native router │ ├── entry/ │ ├── client.ts -> mount vs hydrate, in the browser │ └── server.ts -> renderToString / renderToStream (SSR only) │ ├── components/ │ └── Link.tsx -> type-safe navigation component │ └── pages/ ├── 404.tsx -> rendered for unmatched routes ├── index/ │ └── index.tsx -> the route -> / └── users/ ├── layout.tsx -> wraps everything under /users └── [id]/ └── index.tsx -> the route -> /users/:id ``` ### Foundation files | File | What it is | When you touch it | |---|---|---| | `app.*` | The **global wrapper**, rendered around every route including `404` - the place for providers, auth gates, analytics, an app-wide error boundary. Not a layout: it has no folder scope, it simply wraps everything. | Providers, global chrome | | `router.ts` | `routerFactory` - hands your `app` plus the derived routes to the framework's native router, returning `clientRouter()` for browser navigation and `serverRouter(url)` for SSR. | Rarely | | `entry/client.*` | The browser entry, referenced from `index.html`. `renderFactory` picks `mount()` (fresh render) or `hydrate()` (SSR markup already present) automatically. | Rarely | | `entry/server.*` | The SSR entry, exporting `renderToString` and - where the framework supports it - `renderToStream`. Only present when SSR is enabled. | Injecting SSR assets into `head` | | `components/Link.*` | The typed [Link](/frontend/link-navigation) component: `to` takes a `[routeName, ...params]` tuple, so renaming a route directory becomes a compile error at every call site. | Styling it | | `index.html` | Vite's HTML entry, loading `entry/client`. | Meta tags, fonts, the mount node | | `tsconfig.json` | Extends the derived `lib//tsconfig.json`, which carries JSX and path settings. Anything you set here wins. | [Relaxing strictness](/backend/type-safety) | | `kosmo.config.ts` | The folder's [configuration](/essentials/config) - the `frontend`, `backend` and `validation` blocks, and any Vite option. | Turning on SSR, adding Vite plugins | ### Inside `pages/` | File | What it is | |---|---| | `/index.*` | **The route**. Its folder path becomes the URL. | | `/layout.*` | Wraps that folder and everything beneath it. Only works **inside a route folder**. [Details ›](/frontend/layouts) | | `404.*` | The catch-all [error page](/frontend/error-pages) for unmatched URLs. | | anything else | A colocated helper - never a route. | ### Extensions per framework `router.ts` and `entry/*` are always `.ts`; everything else follows the framework: | | React | SolidJS | Vue | Svelte | MDX | |---|:---:|:---:|:---:|:---:|:---:| | App file | `app.tsx` | `app.tsx` | `app.vue` | `app.svelte` | `app.mdx` | | Page | `index.tsx` | `index.tsx` | `index.vue` | `index.svelte` | `index.mdx` / `.md` | | Layout | `layout.tsx` | `layout.tsx` | `layout.vue` | `layout.svelte` | `layout.mdx` | | Error page | `404.tsx` | `404.tsx` | `404.vue` | `404.svelte` | `404.mdx` | | `Link` | `Link.tsx` | `Link.tsx` | `Link.vue` | `Link.svelte` | `Link.tsx` | | Extra | – | – | – | – | `components/mdx.ts` (component map) | A source folder runs exactly one framework and ignores the others' files - a Vue folder never picks up a stray `.tsx` page. [Full matrix ›](/essentials/frameworks#frontends) Derived code - the route table, fetch clients, validators - lives in `lib/`, is git-ignored, and is not something you read to learn the project. [Why codegen ›](/essentials/why-codegen) ## 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)* | | Svelte | n/a *(no JSX - compiled from `.svelte`)* | | MDX | `"preact"` | KosmoJS delegates JSX transformation to Vite, not TypeScript - but differing `jsxImportSource` values cause type conflicts when multiple frameworks coexist in the same project. Solved by deriving a `tsconfig.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.json" } ``` Each config supplies the correct `jsxImportSource`, path mappings, and core settings. ## What Differs Between Frameworks Routing, layouts, validation and the fetch clients behave identically everywhere. Data loading, streaming support, SSG, TanStack Query and the exotic routing syntaxes do not - those differences are collected in one table: [Framework Support Matrix ›](/essentials/frameworks#frontends) --- --- url: /frontend/application.md description: >- Seeded foundation files for React, SolidJS, Vue, Svelte and MDX applications - root app component, AppProvider seam, router configuration and client entry point with SSR hydration support. --- Creating a source folder seeds 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 A minimal root component is seeded as your application shell. Extend it with global layouts, error boundaries, authentication providers, or other application-wide concerns. The shell composes `AppProvider`, imported from `_/app`, around the routed tree. `_/app` is a derived seam: a wrapper KosmoJS owns and can swap under the hood. By default it is a pass-through (it renders its children unchanged), so out of the box your app behaves exactly like a plain shell. ### Root Application Component - React ```tsx // React: app.tsx import { Outlet } from "react-router"; import { AppProvider } from "_/app"; export default function App() { return ( ); } ``` ### Root Application Component - Solid ```tsx // Solid: app.tsx import type { ParentComponent } from "solid-js"; import { AppProvider } from "_/app"; const app: ParentComponent = (props) => { return {props.children}; }; export default app; ``` ### Root Application Component - Vue ```vue ``` ### Root Application Component - Svelte ```svelte {@render children()} ``` ### Root Application Component - MDX ```mdx // MDX: app.mdx import { AppProvider } from "_/app"; {props.children} ``` ### Why the AppProvider seam Wrapping the shell in `AppProvider` costs nothing when it is a pass-through, and it buys one thing: features that need to wrap the whole tree in a provider - a query client, a theme, an auth context - can be enabled without you editing your code. The pass-through `_/app` is swapped for one that installs the provider, and the file that composes it is untouched because it already wires `AppProvider` unconditionally. Toggling such a feature on or off never changes your code. A plain shell would force you to add and remove the provider wiring by hand each time. ## Router Configuration The `routerFactory` function in `router.ts` file connects your root app component and derived routes to the framework's native router. It accepts a callback receiving derived 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 ### Router Configuration - React ```tsx // React: router.ts import routerFactory, { createRouters } from "_/router"; import app from "./app"; export default routerFactory((routes) => { const { clientRouter, serverRouter } = createRouters(routes, { app }); return { clientRouter() { return clientRouter() }, serverRouter(url) { return serverRouter(url) }, }; }); ``` ### Router Configuration - Solid ```tsx // Solid: router.ts import routerFactory, { createRouters } from "_/router"; import app from "./app"; export default routerFactory((routes) => { const { clientRouter, serverRouter } = createRouters(routes, { app }); return { clientRouter() { return clientRouter() }, serverRouter(url) { return serverRouter(url) }, }; }); ``` ### Router Configuration - Vue ```ts // Vue: router.ts import routerFactory, { createRouters } from "_/router"; import { appProvider } from "_/app"; import app from "./app.vue"; export default routerFactory((routes) => { const { clientRouter, serverRouter } = createRouters(routes, { app, use: [[appProvider, undefined]], }); return { clientRouter() { return clientRouter() }, serverRouter(url) { return serverRouter(url) }, }; }); ``` ### Router Configuration - Svelte ```svelte import routerFactory, { createRouters } from "_/router"; import app from "./app.svelte"; export default routerFactory((routes) => { const { clientRouter, serverRouter } = createRouters(routes, { app }); return { clientRouter() { return clientRouter() }, serverRouter(url) { return serverRouter(url) }, }; }); ``` ### Router Configuration - MDX ```tsx // MDX: router.ts import routerFactory, { createRouters } from "_/router"; import app from "./app.mdx"; import { components } from "./components/mdx" export default routerFactory((routes) => { const { clientRouter, serverRouter } = createRouters(routes, { app, components }); return { clientRouter() { return clientRouter() }, serverRouter(url) { return serverRouter(url) }, }; }); ``` ### Router Configuration (cont.) The derived `routes` are always wrapped inside your `app` component, establishing the layout hierarchy. ## Application Entry The `entry/client.ts` 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 `__KOSMO_HYDRATION_BOOL__` flag to select the correct method: `hydrate()` for SSR hydration, `mount()` for a fresh client-only mount. ### Application Entry - React ```tsx // React: entry/client.ts import renderFactory, { createRoutes, hydrate, mount, } 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 { hydrate() { return hydrate(() => clientRouter(), root); }, mount() { return mount(() => clientRouter(), root); }, }; }); } else { console.error("❌ Root element not found!"); } ``` ### Application Entry - Solid ```tsx // Solid: entry/client.ts import renderFactory, { createRoutes, hydrate, mount, } 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 { hydrate() { return hydrate(() => clientRouter(), root); }, mount() { return mount(() => clientRouter(), root); }, }; }); } else { console.error("❌ Root element not found!"); } ``` ### Application Entry - Vue ```ts // Vue: entry/client.ts import renderFactory, { createRoutes, hydrate, mount, } from "_/entry/client"; import routerFactory from "../router"; const routes = createRoutes(); const { clientRouter } = routerFactory(routes); const root = document.getElementById("app"); if (root) { renderFactory(() => { return { hydrate() { return hydrate(() => clientRouter(), root); }, mount() { return mount(() => clientRouter(), root); }, }; }); } else { console.error("❌ Root element not found!"); } ``` ### Application Entry - Svelte ```svelte import renderFactory, { createRoutes, hydrate, mount, } from "_/entry/client"; import routerFactory from "../router"; const routes = createRoutes(); const { clientRouter } = routerFactory(routes); const root = document.getElementById("app"); if (root) { renderFactory(() => { return { hydrate() { return hydrate(() => clientRouter(), root); }, mount() { return mount(() => clientRouter(), root); }, }; }); } else { console.error("❌ Root element not found!"); } ``` ### Application Entry - MDX ```tsx // MDX: entry/client.ts import renderFactory, { createRoutes, hydrate, mount, } from "_/entry/client"; import routerFactory from "../router"; const routes = createRoutes(); const { clientRouter } = routerFactory(routes); const root = document.getElementById("app"); if (root) { renderFactory(() => { return { hydrate() { return hydrate(() => clientRouter(), root); }, mount() { return mount(() => clientRouter(), root); }, }; }); } else { console.error("❌ Root element not found!"); } ``` ### Application Entry (cont.) Under the hood: * 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`. * Svelte uses `mount`/`hydrate` from `svelte`. * MDX uses `render`/`hydrate` from `preact`. The derived `hydrate` and `mount` are conveniences that wire the router to the DOM the usual way - nothing more. If you need custom mounting, ignore them and build the component yourself: the entry only needs to render the router's component into `root`. Read the derived `_/entry/client` source to see exactly what they do, then substitute your own. --- --- url: /frontend/routing.md description: >- Automatic route registration, lazy-loaded components, data loading integration, and nested layout patterns for React, SolidJS, Vue, Svelte and MDX applications. --- Dev server continuously watches your `pages/` directory for new or updated pages. You never wire routes by hand: as page components are created, updated or deleted, a matching route configuration is written into `lib/` for the native router to consume. ## 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) ## Routes There is no central route tree for you to register or maintain - no `routeTree.gen.ts` to import, no route config object to keep in sync. Route definitions are written into `lib//` and the framework's own router consumes them; you reach them only through `createRoutes()` in your entry file, which the seeded boilerplate already wires up. React, SolidJS and Vue each get a plain, **framework-native** route definition - the same object you would have hand-written for that router. Svelte and MDX have no third-party router, so KosmoJS supplies the matcher and emits its own `RawRoute` shape instead. Because the output is native, everything your router documents keeps working: lazy loading, nested layouts, navigation guards, `loader`/`preload`, error elements. [Details ›](/routing/intro#native-routing-under-the-hood) ## 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. ## Data Loading on Navigation Every framework integrates data fetching into the route lifecycle through a page-level `loader`/`preload` export. **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 (wrap the fetch in `query()` so both share one cache key), so no duplicate requests are made. **Vue** - a page exports a `loader` (from a plain ` ``` ### Layout Implementation - Svelte ```svelte // Svelte: layout.svelte
{@render children()}
...
``` ### Layout Implementation - MDX ```mdx // MDX: layout.mdx
{props.children}
Built with KosmoJS
``` ### Layout Implementation (cont.) React renders child routes via ``. SolidJS and MDX use `props.children`, Svelte renders `{@render children()}`, and Vue uses ``. ## Data Loading in Layouts Layout data loading follows the same per-framework patterns as page components, but how a layout's data stays distinct from its child page's differs: * **React** scopes structurally - each route (layouts included) owns its `loader`, and `useLoaderData()` returns the calling route's data. No key; the route tree carries the identity. * **SolidJS** keys by the `query()` cache string you supply (`"dashboard/data"` here), so the key lives in the `query()` wrapper, not the hook read. * **Vue, Svelte, and MDX** share one per-route store keyed by route name, so the layout passes its path-qualified name to `useLoaderData` (a page passes nothing) - the hook can't tell which layout it runs in. ### Data Loading in Layouts - React ```tsx // React: layout.tsx import { Outlet, useLoaderData } from "react-router"; import fetchClients from "_/fetch"; const { GET } = fetchClients["dashboard/data"]; export const loader = () => GET(); export default function Layout() { const data = useLoaderData(); // ... return ; } ``` ### Data Loading in Layouts - Solid ```tsx // Solid: layout.tsx import { Suspense, type ParentComponent } from "solid-js"; import { createAsync, query } from "@solidjs/router"; import fetchClients from "_/fetch"; const { GET } = fetchClients["dashboard/data"]; // wrap in query() so preload and createAsync share one cache key const getData = query(() => GET(), "dashboard/data"); export const preload = () => getData(); const Layout: ParentComponent = (props) => { const data = createAsync(() => getData()); // ... return {props.children}; }; export default Layout; ``` ### Data Loading in Layouts - Vue ```vue ``` ### Data Loading in Layouts - Svelte ```svelte
{@render children()}
``` ### Data Loading in Layouts - MDX ```mdx // MDX: layout.mdx import fetchClients from "_/fetch"; import { useLoaderData } from "_/use"; const { GET } = fetchClients["dashboard/data"]; export const loader = () => GET(); export const Nav = () => { // a layout passes its path-qualified name to read its own data const data = useLoaderData("dashboard/layout"); return ; };
}>
{query.data?.name}
); } ``` ### Basic Usage - Vue ```vue ``` ### Basic Usage - Svelte ```svelte {#if query.isPending}
Loading...
{:else}
{query.data?.name}
{/if} ``` ### Basic Usage (cont.) The read hook is where the frameworks differ most: * React and Vue take the options object directly * Solid and Svelte take a thunk (`() => (...)`) so the options stay reactive * Svelte's hook is `createQuery`, not `useQuery` * Svelte Query v6 uses runes, so the result is read directly (`query.data`) with no `$` prefix. * The `queryKey`/`queryFn` shape is the same everywhere. ## SSR Warmup (advanced) Everything above works without any SSR wiring: `useQuery` fetches on the client after mount. That is the seamless path, and for most pages it is enough. If you want a page's data rendered on the server and hydrated warm on the client, you wire it yourself with TanStack Query's own SSR primitives - `dehydrate` on the server, `HydrationBoundary` on the client. This is intentional: KosmoJS provides the client and gets out of the way, so you use TanStack's documented, framework-native APIs directly. The mechanism is the same across frameworks - prefetch into the request client, `dehydrate` it, carry the snapshot to the client, `hydrate` (or wrap in `HydrationBoundary`) - but the exact idiom is each adapter's own. Follow your framework's official SSR guide: * React - https://tanstack.com/query/latest/docs/framework/react/guides/ssr * Solid - https://tanstack.com/query/latest/docs/framework/solid/guides/ssr * Vue - https://tanstack.com/query/latest/docs/framework/vue/guides/ssr * Svelte - https://tanstack.com/query/latest/docs/framework/svelte/ssr The one KosmoJS-specific detail: get the request-scoped client from `getQueryClient()` (import from `_/query`) in your loader, so the client you prefetch into is the same one the render reads. Use one shared query-options helper in both the loader and the component so the `queryKey` matches - prefetch under one key and read under another and the cache misses, forcing a refetch. Sketches per framework - see the official guides above for the full picture: ### SSR Warmup (advanced) - React ```tsx // React: pages/users/[id]/index.tsx // Prefetch in the loader, dehydrate, then wrap the page in HydrationBoundary. // https://tanstack.com/query/latest/docs/framework/react/guides/ssr import { dehydrate, HydrationBoundary, useQuery } from "@tanstack/react-query"; import { useLoaderData, useParams } from "react-router"; import { getQueryClient } from "_/query"; import fetchClients from "_/fetch"; const { GET } = fetchClients["users/[id]"]; const queryOptions = (id: string) => ({ queryKey: ["users", id] as const, queryFn: () => GET([id]), }); export const loader = async ({ params }: { params: { id: string } }) => { const client = getQueryClient(); await client.prefetchQuery(queryOptions(params.id)); return dehydrate(client); }; function User() { const { id } = useParams() as { id: string }; const { data } = useQuery(queryOptions(id)); return
{data?.name}
; } export default function Page() { const state = useLoaderData(); return ( ); } ``` ### SSR Warmup (advanced) - Solid ```tsx // Solid: pages/users/[id]/index.tsx // Solid Query rehydrates through Solid's generateHydrationScript(), // which the SSR entry already emits - so there is no HydrationBoundary to place. // Prefetch into the request client and the cache crosses to the browser automatically. // https://tanstack.com/query/latest/docs/framework/solid/guides/ssr import { useQuery } from "@tanstack/solid-query"; import { useParams } from "@solidjs/router"; import { getQueryClient } from "_/query"; import fetchClients from "_/fetch"; const { GET } = fetchClients["users/[id]"]; const queryOptions = (id: string) => ({ queryKey: ["users", id], queryFn: () => GET([id]), }); export const preload = ({ params }: { params: { id: string } }) => getQueryClient().prefetchQuery(queryOptions(params.id)); export default function Page() { // Solid folders read params through Solid Router's own hook const params = useParams(); const query = useQuery(() => queryOptions(params.id)); return
{query.data?.name}
; } ``` ### SSR Warmup (advanced) - Vue ```vue ``` ### SSR Warmup (advanced) - Svelte ```svelte
{query.data?.name}
``` ### SSR Warmup (advanced) (cont.) `dehydrate` returns a snapshot of the request client's cache; import it from your `@tanstack/{framework}-query` package. On React, Vue and Svelte you carry that snapshot to the client (via the loader data) and hand it to `HydrationBoundary` or `hydrate`, which merges it into the client cache before components read. Solid needs none of this - `generateHydrationScript()` carries the cache for you. ## Mutations and Invalidation Mutations use the same client and need no KosmoJS-specific wiring. `invalidateQueries` refetches affected queries in place - the thing a loader alone cannot do without re-navigating. ### Mutations and Invalidation - React ```tsx // React: components/RenameUser.tsx import { useMutation, useQueryClient } from "@tanstack/react-query"; import fetchClients from "_/fetch"; const { POST } = fetchClients["users/[id]"]; export default function RenameUser({ id }: { id: string }) { const qc = useQueryClient(); const rename = useMutation({ mutationFn: (name: string) => POST([id], { name }), onSuccess: () => qc.invalidateQueries({ queryKey: ["users", id] }), }); return ; } ``` ### Mutations and Invalidation - Solid ```tsx // Solid: components/RenameUser.tsx // Solid's hooks take a thunk, like useQuery above import { useMutation, useQueryClient } from "@tanstack/solid-query"; import fetchClients from "_/fetch"; const { POST } = fetchClients["users/[id]"]; export default function RenameUser(props: { id: string }) { const qc = useQueryClient(); const rename = useMutation(() => ({ mutationFn: (name: string) => POST([props.id], { name }), onSuccess: () => qc.invalidateQueries({ queryKey: ["users", props.id] }), })); return ; } ``` ### Mutations and Invalidation - Vue ```vue ``` ### Mutations and Invalidation - Svelte ```svelte ``` ### Mutations and Invalidation (cont.) The shape is identical across frameworks - swap `useMutation`/`useQueryClient` for the `@tanstack/{solid,vue,svelte}-query` equivalents. Mutations are client-side, so SSR does not affect them. ## Configuring a custom client By default the provider uses the client from `getQueryClient()`, which needs no configuration. When you want custom defaults - a global `staleTime`, retry policy, and so on - build the client once with `createQueryClient(options)` and the provider picks it up. `createQueryClient` both builds the configured client and registers it as the active one, so every later `getQueryClient()` returns that same instance. `getQueryClient()` takes no options, so there is no way to pass options that silently get ignored. Where you call it differs slightly by framework - it goes wherever your app is composed: ### Configuring a custom client - React ```tsx // React: app.tsx - pass the configured client to the provider's `client` prop import { AppProvider } from "_/app"; import { createQueryClient } from "_/query"; import { Outlet } from "react-router"; const client = createQueryClient({ defaultOptions: { queries: { staleTime: 60_000 } }, }); export default function App() { return ( ); } ``` ### Configuring a custom client - Solid ```tsx // Solid: app.tsx - pass the configured client to the provider's `client` prop import { AppProvider } from "_/app"; import { createQueryClient } from "_/query"; import type { ParentComponent } from "solid-js"; const client = createQueryClient({ defaultOptions: { queries: { staleTime: 60_000 } }, }); const app: ParentComponent = (props) => { return {props.children}; }; export default app; ``` ### Configuring a custom client - Vue ```vue ``` ### Configuring a custom client - Svelte ```svelte {@render children()} ``` ### Configuring a custom client (cont.) The `client` prop is typed as the adapter's `QueryClient` and exists only when the option is enabled - so `` type-checks exactly where you would write it. Omit it and the default client is used. ## Streaming The warm-SSR path here is exact under string rendering (the default). Streaming (`renderToStream`) needs the framework's streamed-hydration boundary to capture queries that resolve mid-stream; until you move a route to streaming, string mode gives correct, fully-warm SSR. --- --- url: /frontend/link-navigation.md description: >- 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, Svelte and MDX. --- The `Link` component 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,vue,svelte}` 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: ### Usage - Menu.tsx ```tsx // Menu.tsx import Link from "~/components/Link"; export default function Menu() { return ( ); } ``` ### Usage - Menu.vue ```vue ``` ### Usage - Menu.svelte ```svelte ``` ## LinkProps Type The `to` prop is typed as `LinkProps` - a discriminated union derived 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: /frontend/custom-templates.md description: >- Override default seeded page components for specific routes using glob pattern matching. Seed specialized boilerplate for landing pages, admin dashboards, marketing sections etc. --- Every frontend framework supports template overrides for specific routes through pattern-based matching. Useful for standardizing structure across landing pages, admin tools, or any section requiring a consistent starting point. Templates seed page `index` files - `pages/**/index.*` - and nothing else. Every other seeded file gets the built-in minimal boilerplate to start from. ## Configuration Pass custom templates through the `frontend` block in your source folder's `kosmo.config.ts`: ```ts [kosmo.config.ts] import { defineConfig } from "@kosmojs/dev"; // [!code ++:8] const landingTemplate = ` export default function Page() { return (

Welcome

); }`; export default defineConfig({ frontend: { stack: "react", base: "/front", templates: { // [!code ++:4] "landing/*": landingTemplate, "marketing/**": landingTemplate, }, }, }); ``` Now every new route under `landing/` and `marketing/` will start with your template. > **Templates only fill blank files.** > Boilerplate is written into a file **only when that file is empty**. > It never overwrites work you have already done - which is also why changing a template does not retroactively rewrite existing pages. > To re-seed one, empty the file and it will be filled again. ## 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 - in the order the keys are written, so order them most specific first: ```ts templates: { "landing/home": homeTemplate, // highest specificity "landing/*": landingTemplate, // medium specificity "**": fallbackTemplate, // lowest specificity } ``` ::: warning Numeric-looking patterns jump the queue JavaScript objects order integer-like keys first, regardless of where you wrote them - so a pattern such as `"2024/**"` is hoisted to the front and matches before anything above it. Prefix it with `./` to keep your written order: `"./2024/**"`. The `./` is stripped before matching. The same applies to [renderMode](/frontend/server-side-render#selecting-the-render-mode), which uses the same resolver. ::: ## Parameter Compatibility Route parameters are escaped before matching, so `[id]`, `{id}` and `{...path}` mean themselves rather than glob syntax: ```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 written to disk as the page component file. A template can be either a **plain string**, or a **function** receiving the resolved route and returning the string - useful when the output depends on the route itself: ```ts templates: { // a plain string "landing/*": landingTemplate, // or a function of the route "admin/**": (route) => ` export default function Page() { return

${route.name}

; }`, } ``` Each framework has its own component structure: ### Template Format - React ```ts // React: kosmo.config.ts const customTemplate = ` import { useParams } from "react-router"; export default function Page() { const params = useParams(); return (

Custom Template

Route params: {JSON.stringify(params)}

); } `; ``` ### Template Format - Solid ```ts // Solid: kosmo.config.ts const customTemplate = ` import { useParams } from "@solidjs/router"; export default function Page() { const params = useParams(); return (

Custom Template

Route params: {JSON.stringify(params)}

); } `; ``` ### Template Format - Vue ```ts // Vue: kosmo.config.ts const customTemplate = ` `; ``` ### Template Format - Svelte ```ts // Svelte: kosmo.config.ts const customTemplate = `

Custom Template

Route params: {JSON.stringify(params)}

`; ``` ### Template Format - MDX ```mdx // MDX: kosmo.config.ts import { useParams } from "_/use"; # Custom Template Route params: {JSON.stringify(useParams())} ``` ### Template Format (cont.) > Templates use Handlebars syntax for any dynamic content injected during seeding. > 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 frontend: { stack: "react", base: "/front", templates: { "landing/**": landingTemplate, "marketing/**": marketingTemplate, "promo/**": promoTemplate, }, } ``` ### Admin Interfaces ```ts frontend: { stack: "react", base: "/front", templates: { "admin/**": adminTemplate, }, } ``` ## Default Template Override Routes without a matching pattern use the built-in default, which displays the route name as a placeholder. Replace it globally with: ```ts templates: { "**": myDefaultTemplate, } ``` --- --- url: /frontend/mdx.md description: >- Create content-focused source folders with MDX - static HTML rendering with Preact, nested layouts, frontmatter-driven head injection, typed Link component. --- 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. ## Enabling MDX MDX is enabled automatically when you create a source folder and select MDX as the framework. To add it to an existing folder: ```ts [kosmo.config.ts] import { defineConfig } from "@kosmojs/dev"; export default defineConfig({ frontend: { stack: "mdx", base: "/docs", ssr: true, ssg: true, }, }); ``` ## 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 in pages and layouts via `useFrontmatter()` (or `useRoute().frontmatter`). ## 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: ### Using Components - pages/blog/Alert.tsx ```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}
); } ``` ### Using Components - pages/blog/index.mdx ```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.ts`: ```tsx [components/mdx.ts] 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 the wrapped content as `props.children`. Everything else -
the page's frontmatter, loader data - is read with hooks, so `props` carries
only what a layout composes around:

```mdx [pages/docs/layout.mdx]


{props.children}
Built with KosmoJS
``` Access the page's frontmatter with `useFrontmatter()` for dynamic head content or conditional rendering: ```mdx [pages/layout.mdx] import { useFrontmatter } from "_/use"; export const Header = () => { const frontmatter = useFrontmatter(); return frontmatter.title ? (

{frontmatter.title}

) : null; };
{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()` from `_/use`. `.mdx` is not TypeScript, so it takes no type argument and the params come back untyped - move anything that needs the typed form into a `.tsx` component and import it: ```mdx [pages/blog/post/[slug]/index.mdx] import { useParams } from "_/use"; export const Post = () => { const { slug } = useParams(); return

Reading: {slug}

; }; # Blog post ``` Optional parameters come back possibly-undefined, and a splat parameter comes back as an array of segments or undefined: ```mdx [pages/blog/{category}/{tag}/index.mdx] import { useParams } from "_/use"; export const Filters = () => { const { category, tag } = useParams(); return

{category ?? "all"} / {tag ?? "all"}

; }; ``` Note the call sits **inside a component**, not at module scope - see the warning below. `useRoute()` provides the full route context including name, params, frontmatter, and loader data: ```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. ## Data Fetching Pages can fetch data during render via a `loader` export - a function that runs before the page is rendered, on both the server and the client. It receives the resolved route as first argument, and its return value is read inside the page with the `useLoaderData()` hook - `props` stays entirely yours. ```mdx [pages/users/index.mdx] import f from "_/fetch"; import { useLoaderData } from "_/use"; export const loader = f["users"].GET; export const Message = () => { const data = useLoaderData(); return

The message is: {data.msg}

; }; # Welcome ``` `loader` fetches through the same client used elsewhere in the project, so a request made during SSR is captured and replayed on hydration instead of firing twice - no extra wiring needed on the page. ### Loaders with Route Parameters `loader` runs before the page tree exists, so it can't use `useParams()`/`useRoute()` - those are hooks, and hooks only work while Preact is actually rendering a component. Instead, `loader` receives the resolved route object directly as first argument: ```ts // the object passed to `loader` - a subset of the route context, // without frontmatter or loaderData (which aren't resolved yet at loader time) type LoaderRoute = { name: string; params: Record>; paramsEntries: [keys: Array, values: Array]; }; ``` `paramsEntries` is a `[keys, values]` tuple, both in the same order the route declares its parameters - the same order `GET` expect: ```mdx [pages/blog/[slug]/index.mdx] import f from "_/fetch"; import { useLoaderData } from "_/use"; export const { GET } = f["blog/[slug]"]; export const loader = ({ paramsEntries }) => { const [keys, params] = paramsEntries; return GET(params); }; export const Title = () => { const data = useLoaderData(); return

{data.title}

; }; ``` Pass `params` straight to a parametrized endpoint when only positional values are needed; `keys` is there alongside it for cases like building a request body or a cache key from the parameter names themselves. Either way, there's no need to reconstruct an array from `params`/`useParams()` by hand. > **Don't** reach for `Object.keys(route.params)`/`Object.values(route.params)` > as a substitute. It happens to work today because JS object key order > usually follows insertion order, but that's an implicit contract, not a > guarantee tied to how the route declares its parameters - it can silently > break for multi-param or splat routes, or if matching internals ever > change. `paramsEntries` derives its order from the route's own declared > parameter list, so it's correct by construction instead of by coincidence. > **Important:** `loader` runs during resolution, before the component tree > is built, so it never has access to `useParams()`, `useRoute()`, or any > other hook - only to the `Route` object passed as its argument. Hooks > remain the right tool inside actual components; > `loader` is a pre-render step, not a rendered component. ## Navigation with `Link` There is `Link` component at `components/Link.tsx` for your convenience: ```mdx import Link from "~/components/Link" Navigate to the <Link to={["blog/[slug]", "hello-world"]}>first post</Link> or go <Link to={["index"]}>home</Link>. ``` The `to` prop accepts the same tuple as other frameworks - route name followed by parameters. `Link.tsx` is type-checked, but the call site in an `.mdx` page is not, so a wrong route name or a missing parameter surfaces at runtime rather than at build time. Wrap navigation in a `.tsx` component where that matters. > **Tip:** When `Link` is enabled in `components/mdx.ts` (the default), > it can be used in pages without import - it is a global component provided via `MDXProvider`. ## Frontmatter & Head Injection Frontmatter drives `<head>` 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 <head> <title>Getting Started ``` This follows the same convention used by VitePress - no new syntax to learn. ## Application Structure MDX folders get the same seeded foundation files as other frameworks, keeping project structure consistent: ```txt src/content/ ├── app.mdx ← global layout ├── router.ts ← routes wired into the native router ├── index.html ← HTML shell with placeholders ├── components/ │ ├── Link.tsx ← navigation component │ └── mdx.ts ← MDXProvider component overrides ├── entry/ │ ├── client.ts ← minimal client entry │ └── server.ts ← SSR rendering with Preact └── pages/ └── *.mdx ← content pages, optionally exporting `loader` ``` ### Router Configuration `router.ts` follows the same `routerFactory` pattern as every other framework - the only MDX-specific part is the `components` map handed to `createRouters` alongside the app, so the MDXProvider overrides apply to every page. ```ts [router.ts] import routerFactory, { createRouters } from "_/router"; import app from "./app.mdx"; import { components } from "./components/mdx" export default routerFactory((routes) => { const { clientRouter, serverRouter } = createRouters(routes, { app, components }); return { clientRouter() { return clientRouter() }, serverRouter(url) { return serverRouter(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 }`. MDX renders static content, so it implements only `renderToString` - it is the one framework that omits `renderToStream`. #### Client/Server Entry - entry/client.ts ```ts // entry/client.ts import renderFactory, { createRoutes, hydrate, mount, } from "_/entry/client"; import routerFactory from "../router"; const routes = createRoutes(); const { clientRouter } = routerFactory(routes); const root = document.getElementById("app"); if (root) { renderFactory(() => { return { hydrate() { return hydrate(() => clientRouter(), root); }, mount() { return mount(() => clientRouter(), root); }, }; }); } else { console.error("❌ Root element not found!"); } ``` #### Client/Server Entry - entry/server.ts ```ts // entry/server.ts import renderFactory, { createRoutes, renderToString, // no renderToStream on MDX folders } from "_/entry/server"; import routerFactory from "../router"; const routes = createRoutes(); const { serverRouter } = routerFactory(routes); export default renderFactory(() => { return { async renderToString(url, { assets }) { return renderToString( () => serverRouter(url), { headerTags: assets.map(({ tag }) => tag) }, ); }, }; }); ``` ## 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. * **`loader` can't use hooks.** `useParams()`, `useRoute()`, and any other hook only work inside a rendered component. `loader` runs before the tree exists - use the `Route` object passed as its argument (`paramsEntries`, `frontmatter`, etc.) instead. * **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: /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 calls its approach **runtype validation**: your TypeScript types are automatically converted into JSON Schema and validated at runtime. > "Runtype" is a shorthand, not an industry standard you're expected to already know - a *run*time check derived from a *type*. > If you've used Zod or Yup, this inverts the direction: instead of writing a schema and inferring a type from it, > you write the type and the schema is derived from it. 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 typed 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 produce 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. 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 Derivation Works KosmoJS uses AST parsing to extract types from your route files, then AOT compilation to emit 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 derived 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 derived code to use validation. If you're curious about performance characteristics or need to troubleshoot, see [Validation Performance](/validation/performance). --- --- 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. ::: tip Numeric params are coerced for you A URL param arrives as a string, but when you type it as `number`, KosmoJS coerces it before validation - `"123"` becomes `123`. A non-numeric value like `"abc"` is left as-is and fails the number check, producing a clean 400. So you write `number` (or a numeric `VRefine`) and read a real number from `ctx.validated.params`; no manual coercion needed. ::: Access validated parameters through `ctx.validated.params` - it carries the refined type, not the raw string. The underlying raw params still exists if you need the original: * Hono - `ctx.req.param()` * H3 - `event.context.params` * Koa - `ctx.params` 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. ## Overriding the Default Params validation runs in the `validate:params` slot. Claim that slot and your middleware runs instead of it. Use it when no refinement can express the check. Resolving a slug against the database, for instance. Only that target changes. `ctx.validated.params` is no longer set. Every other target keeps its own validator. [Details ›](/backend/middleware#overriding-validation) --- --- 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 and disabled automatically. ## Coerced values Everything off the wire is a string. Some primitive values are coerced automatically, when typed accordingly and content is appropriate. **Numbers - coerced in route params and `query`.** Typing a field as `number` turns `"10"` into `10`. ```ts GET<{ query: { page?: number; }; // coerced: "2" -> 2 }> ``` **Booleans** - coerced in **`query`** only. Typing a field as `boolean` turns `"true"`/`"false"` into `true`/`false`. ```ts GET<{ query: { draft?: boolean; }; // coerced: "true" -> true }> ``` > Params are path segments where a boolean is meaningless, so they never coerce booleans. Coercion only maps the exact expected forms: * a `number` field coerce only numeric values * a `boolean` field coerce only `"true"`/`"false"` values Everything else stays a string and fails validation cleanly. `headers`/`cookies`/`form`/`raw` never coerce. `json` carries real numbers and booleans natively, no coercion needed. ## 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/checkout/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<"checkout">(({ 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 }), ]); ``` Generics are resolved and every referenced type traced, producing 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) => {}), ]); ``` ## Overriding the Default Every target has its own slot: `validate:json`, `validate:form`, `validate:raw`. Claim one and your middleware runs instead of the built-in validator. Use it for a body format no schema describes, or a check that has to hit the database. Replacing one target leaves the rest alone. And `ctx.bodyparser.()` is cached, so your validator and your handler can both read the body. [Details ›](/backend/middleware#overriding-validation) --- --- url: /validation/response.md description: >- Validate API responses before sending to clients. Catch bugs where handlers return incomplete objects, wrong types, or unexpected structures. Enabled by default in development; opt in per handler for production. --- 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. ## Development vs Production Response validation is environment-aware: * **Development / test**: every declared response schema is validated at runtime. Opt out per handler with [runtimeValidation: false](/validation/skip-validation). * **Production builds**: response validation is **disabled by default**. To enable it running in production, set `runtimeValidation: true` on the response target: ```ts [api/users/index.ts] export default defineRoute<"users">(({ GET }) => [ GET<{ response: [200, "json", User], }, { response: { // [!code ++:3] validate this response in production too runtimeValidation: true, } }>(async (ctx) => { // ... }), ]); ``` There is no global switch - each handler enables production response validation for itself. This is deliberate: validating every outgoing response costs CPU on your hottest path, so production validation is a per-endpoint decision, not a blanket default. Note the asymmetry with request validation: payloads and parameters coming *into* your API are always validated, in every environment (unless explicitly skipped) - they cross a trust boundary. Responses are produced by your own code, so by default they are checked only where bugs are cheap: in development. Response validation is especially valuable for data sourced from databases or third-party APIs, where the shape can change without warning. If an endpoint serves such data and malformed output would be worse than a thrown error, that endpoint is a candidate for `runtimeValidation: true`. Defining a response schema also enables automatic `OpenAPI` derivation - type safety and documentation in one step. [Details ›](/openapi) ## Overriding the Default Response validation runs in the `validate:response` slot. Claim that slot and your middleware decides what a valid response looks like. The built-in check stops running. Use it for responses no schema can describe - a stream, for example. [Details ›](/backend/middleware#overriding-validation) --- --- 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 any type - primitives, arrays, and objects alike. ```ts VRefine ``` > It is globally available - no import needed. 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). The full keyword set, grouped as in the spec, is below - everything at a glance. ## Validation keywords ### Strings | Keyword | Meaning | | --- | --- | | `minLength` | Minimum character count | | `maxLength` | Maximum character count | | `pattern` | Must match the ECMA-262 regular expression | | `format` | Must match a named format - see [the format table](#formats) | ```ts VRefine VRefine VRefine ``` ### Numbers | Keyword | Meaning | | --- | --- | | `minimum` | Inclusive lower bound | | `maximum` | Inclusive upper bound | | `exclusiveMinimum` | Exclusive lower bound | | `exclusiveMaximum` | Exclusive upper bound | | `multipleOf` | Must be evenly divisible by the given value | ```ts VRefine VRefine ``` ### Arrays | Keyword | Meaning | | --- | --- | | `minItems` | Minimum element count | | `maxItems` | Maximum element count | | `uniqueItems` | All elements must be distinct | | `contains` | At least one element must match the given schema, written inline as a schema literal | | `minContains` / `maxContains` | Bounds on how many elements may match `contains` | ```ts VRefine, { minItems: 1, maxItems: 20 }> VRefine>, { uniqueItems: true }> // at least one "admin" entry VRefine // one or two elements matching the nested schema - any keyword combination works, // including pattern, enum, format, length bounds VRefine ``` ### Objects | Keyword | Meaning | | --- | --- | | `minProperties` | Minimum property count | | `maxProperties` | Maximum property count | | `required` | Array of mandatory property names - normally implied by TypeScript optionality (`?`), reach for it only on open shapes like `Record` | | `dependentRequired` | Properties required when another property is present | ```ts VRefine, { maxProperties: 20 }> ``` ### Any instance type | Keyword | Meaning | | --- | --- | | `enum` | Must equal one of the listed values - as a top-level constraint a TypeScript literal union (`"a" \| "b"`) expresses this natively; inside a nested schema like `contains` it is the way to say it | | `const` | Must equal a specific value - as a top-level constraint a TypeScript literal type (`"a"`, `2`, `true`) expresses this natively; inside a nested schema like `contains` it is the way to say it | | `type` | Constrains the JSON type - redundant at the top level (the base type already is the type), required inside nested schemas like `contains` | ### Content keywords `contentEncoding`, `contentMediaType`, and `contentSchema` describe how to interpret string contents (e.g. base64 payloads). Per the spec they are **annotations, not assertions** - they document, they don't reject. Fine to attach for OpenAPI output; don't expect them to validate anything. ## Formats `format: "..."` values are validated at runtime. Every format defined by the 2020-12 spec is supported, plus two TypeBox extras at the end: | Format | Validates | | --- | --- | | `date-time` | RFC 3339 timestamp, e.g. `2026-08-23T07:00:00Z` | | `date` | Full date, e.g. `2026-08-23` | | `time` | Time of day, e.g. `07:00:00Z` | | `duration` | ISO 8601 duration, e.g. `P3DT4H` | | `email` | Email address | | `idn-email` | Internationalized email address | | `hostname` | DNS hostname | | `idn-hostname` | Internationalized hostname | | `ipv4` | IPv4 address | | `ipv6` | IPv6 address | | `uri` | Absolute URI | | `uri-reference` | URI or relative reference | | `iri` | Internationalized URI | | `iri-reference` | IRI or relative reference | | `uuid` | UUID, e.g. `f81d4fae-7dec-11d0-a765-00a0c91e6bf6` | | `uri-template` | RFC 6570 URI template | | `json-pointer` | JSON Pointer, e.g. `/foo/0` | | `relative-json-pointer` | Relative JSON Pointer | | `regex` | ECMA-262 regular expression source | | `url` | URL (TypeBox extra, not in the spec) | | `json-pointer-uri-fragment` | JSON Pointer in URI fragment form (TypeBox extra) | ```ts VRefine // params: id: must be a valid UUID VRefine // json: from -> email: must be a valid email address ``` ## Integers 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. ## Keep the Wrapping Brackets Literal One rule covers every place KosmoJS reads structure out of your type arguments: ::: tip The rule **The wrapping `[]` and `{}` must be written literally. Anything *inside* them can be aliased.** ::: That applies to three positions: ```ts // the VRefine constraint object VRefine // the params refinement tuple defineRoute<"users/[id]/[action]", [UserID, UserAction]> // the response tuple POST<{ response: [200, "json", User] }> ``` In each case the brackets stay where you can see them, while the values inside are free to be named types - local or imported: ```ts // ✅ contents aliased, brackets kept type Pattern = "^[A-Z]{3}$"; VRefine type UserID = VRefine; defineRoute<"users/[id]", [UserID]> type User = { id: number; name: string }; POST<{ response: [200, "json", User] }> ``` ```ts // ❌ the brackets themselves hidden behind an alias type Params = [UserID, UserAction]; defineRoute<"users/[id]/[action]", Params> type ResponseT = [200, "json", User]; POST<{ response: ResponseT }> ``` The base type of `VRefine` (its first argument) is unrestricted either way - `VRefine` and imported base types resolve normally. ### Why the brackets matter These positions are read **structurally**, from the source: which tuple slot maps to which route parameter, which slot carries the status code versus the body. An alias that hides the brackets gives it an identifier where it expected a shape, and there is nothing to destructure. That failure is silent, and it differs by position: | Position | If the brackets are hidden behind an alias | |---|---| | `params` tuple | the schema does not build - **every** request is rejected with a 400 | | `response` tuple | **no schema is built at all** - response validation never runs, and the route gets no [ResponseT](/fetch/type-safety#response-types) entry | Neither raises a compile error, so nothing points at the alias. If a route rejects input you know is valid, or a response you declared is silently not validated, check the brackets first. > This is one of the mistakes that typecheck cleanly and fail at runtime. > [Silent Failure Checklist ›](/validation/gotchas) --- --- url: /validation/skip-validation.md description: >- Set runtimeValidation option to false to keep TypeScript type checking without runtime validation for gradual adoption, performance optimization, or trusted internal endpoints. --- There are cases where you want `TypeScript` type checking without the runtime validation overhead - rapid iteration on payload structures, or trusted internal endpoints where you control both sides. The second type argument to your handler accepts per-target options. Set `runtimeValidation: false` to keep compile-time types while skipping runtime checks for that target: ```ts [api/example/index.ts] export default defineRoute<"example">(({ POST }) => [ POST<{ json: Payload, }, { json: { runtimeValidation: false, // [!code hl] } }>(async (ctx) => { // ctx.validated.json is not available - use the bodyparser directly const user = await ctx.bodyparser.json() }), ]); ``` This works for both payload and response targets. Route parameter validation cannot be skipped - parameters are part of the URL structure and always validated. For response targets the flag also works the other way: production builds skip response validation unless you set `runtimeValidation: true` on the handler. [Details ›](/validation/response) Use this sparingly. Runtime validation is what catches the bugs `TypeScript` can't - mismatched database responses, unexpected client payloads, API drift. Skipping it is a conscious tradeoff, not a default. --- --- 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 seeded file gives you a working default; customize it freely to add logging, change response formats, or handle specific error types differently. [Details ›](/backend/error-handling) ## 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:** ### ValidationError Properties - Hono ```ts // Hono: api/errors.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" return ctx.json({ error: errorMessage }, 400); } // Field-level errors (useful for form responses) if (error instanceof ValidationError) { const messages = error.errors.map(e => `${e.path}: ${e.message}`); return ctx.json({ error: "validation_error", target: error.target, messages }, 400); } // Log the invalid data, return a summary if (error instanceof ValidationError) { logger.error("Validation failed", { target: error.target, data: error.data }); return ctx.json({ error: error.errorSummary }, 400); } ``` ### ValidationError Properties - H3 ```ts // H3: api/errors.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" event.res.status = 400; return { error: errorMessage }; } // Field-level errors (useful for form responses) if (error instanceof ValidationError) { const messages = error.errors.map(e => `${e.path}: ${e.message}`); event.res.status = 400; return { 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 }); event.res.status = 400; return { error: error.errorSummary }; } ``` ### ValidationError Properties - Koa ```ts // Koa: api/errors.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 derivation, 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). > This is one of four mistakes that typecheck cleanly and fail at runtime. > [Silent Failure Checklist ›](/validation/gotchas) --- --- url: /validation/gotchas.md description: >- The four validation mistakes that typecheck cleanly and fail at runtime - non-inline VRefine constraints, built-in type names, float IDs, and non-coercing targets. --- Most mistakes in KosmoJS are compile errors. Four are not: they typecheck cleanly and then misbehave at runtime. They share a shape - your types are flattened into schema text, and a few things don't survive that trip. **If validation is rejecting data that looks obviously valid, work down this list first.** ## 1. A wrapping bracket hidden behind an alias KosmoJS reads three positions **structurally**, out of the source: the `VRefine` constraint object, the params refinement tuple, and the response tuple. In all three, the wrapping `{}` or `[]` must be written literally. **Anything inside them can be aliased** - local or imported. It is only the brackets themselves that must stay visible. ```ts // ✅ contents aliased, brackets kept type Pattern = "^[A-Z]{3}$"; VRefine type UserID = VRefine; defineRoute<"users/[id]", [UserID]> ``` ```ts // ❌ the brackets themselves behind an alias type Params = [UserID, UserAction]; defineRoute<"users/[id]/[action]", Params> type ResponseT = [200, "json", User]; POST<{ response: ResponseT }> ``` Both forms typecheck. KosmoJS, though, expected a shape and got an identifier, with nothing to destructure - so it fails, silently, and differently per position: | Position | If the brackets are hidden | |---|---| | `params` tuple | schema does not build - **every** request 400s, including valid ones | | `response` tuple | **no schema built** - response validation never runs, and the route gets no `ResponseT` entry | **Symptom:** a route rejects input you know is good, or a response you declared is quietly never validated - with no compile error pointing at the alias. [Details ›](/validation/refine#keep-the-wrapping-brackets-literal) ## 2. A type named after a built-in Name a type `Event`, `Response`, `Date`, `Record`, `Error`, `Buffer` - or any other JS/DOM/TS built-in - and the flattener references the built-in, not your definition. ```ts // ❌ compiles, validates against the DOM Event type Event = { id: number; name: string }; // ✅ type EventT = { id: number; name: string }; type TEvent = { id: number; name: string }; ``` Adopt a `T` prefix or suffix consistently across the project. **Symptom:** validation fails (or passes) in ways that make no sense for the type you wrote, with no compile-time warning. [Full list of names to avoid ›](/validation/naming-conventions) ## 3. `number` where you meant integer Normally, plain `number` allows decimals. A float ID passes validation and is then rejected by your database - turning a clean 400 into a confusing query error further downstream. ```ts // ❌ 1000.5 passes VRefine // ✅ integers only VRefine ``` **Symptom:** a DB driver error instead of a validation error. [Details ›](/validation/refine#integers) ## 4. A non-string type on a target that doesn't coerce Everything off the wire is a string. Only `query` coerces numbers *and* booleans, and route `params` coerce numbers only. `headers`, `cookies`, `form` and `raw` never coerce anything. | Target | `number` | `boolean` | |---|:---:|:---:| | `params` | ✅ coerced | ❌ never (a boolean path segment is meaningless) | | `query` | ✅ coerced | ✅ coerced (`"true"` / `"false"`) | | `json` | ✅ native | ✅ native | | `headers` · `cookies` · `form` · `raw` | ❌ | ❌ | ```ts // ❌ never passes - form values are strings POST<{ form: { age: number; consented: boolean } }> // ✅ accept the wire format, convert in the handler POST<{ form: { age: string; consented: "true" | "false" | "on" | "off" } }> ``` **Symptom:** a form or header field always fails validation no matter what is sent. [Details ›](/validation/payload#coerced-values) ## Two more that aren't silent, but surprise people **A missing `response` means no `ResponseT` entry.** Response typing on the client is opt-in: declare `response` on the handler and you get response validation, an OpenAPI response, and a `ResponseT["route"]["GET"]` entry together. Omit it and you get none of the three. [Details ›](/fetch/type-safety#response-types) **Response validation is off in production by default.** Requests are always validated; responses are validated in development and test only, unless you opt a specific handler in with `runtimeValidation: true`. There is no global switch. [Details ›](/validation/response#development-vs-production) ## When Nothing Above Fits Rebuilding is the blunt instrument, and occasionally the right one. Remove `lib/` dir and restart dev server. That forces a full rebuild of every schema - minutes on a large project, so it is not part of the normal loop. Reach for it when you suspect stale derived output rather than a mistake in your types. [Details ›](/validation/performance#when-it-becomes-noticeable) --- --- 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 derivation 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 derivation step as the tradeoff. Derivation 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 - derivation runs in parallel with the Vite dev server, and results are cached per file. Schemas are only recomputed 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 derivation overhead - because you write the schemas yourself. That eliminates derivation 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 - derivation performance will likely improve further. --- --- url: /fetch/intro.md description: >- KosmoJS automatically derives 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 derives 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 Derived 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 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]); ``` Clients land in the `lib` directory alongside other derived artifacts (validation routines, OpenAPI spec). Everything is updated automatically in the background as you modify routes during development. ## Isomorphic Clients The same client works in both CSR and SSR - identical code, no server functions or RPC layer to define. In the browser it issues a normal same-origin request; during SSR it dispatches to the matched API route in-process, with no network layer and no round-trip latency. [Isomorphic clients ›](/fetch/isomorphic-clients) --- --- url: /fetch/start.md description: >- Import and use KosmoJS 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 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 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. ## The Same Call in CSR and SSR The call above needs no variant for the server. In the browser it issues a same-origin network request; during SSR it dispatches to the API route in-process: ```ts [pages/example/index.tsx] // identical in a client component and in an SSR loader const response = await fetchClients["users/[id]"].GET([123]); ``` Nothing to configure - no separate server-side client, no base URL for the in-process path. [Isomorphic clients ›](/fetch/isomorphic-clients) --- --- url: /fetch/integration.md description: >- Integrate KosmoJS fetch clients with SolidJS query/createAsync, React, Vue and Svelte useLoaderData, and MDX hooks. Type safety flows through all framework abstractions. --- The fetch client returns standard promises, so it fits naturally into whatever async pattern your framework uses. ### The Same Call in CSR and SSR - React ```tsx // React: pages/users/[id]/index.tsx import { useLoaderData } from "react-router"; import fetchClients from "_/fetch"; const { GET } = fetchClients["users/[id]"]; // React Router passes { params, request } into the loader; read params the // native way and pass them to the client as an array export const loader = ({ params }) => GET([params.id]); export default function UserProfile() { const user = useLoaderData(); return
{user.name}
; } ``` ### The Same Call in CSR and SSR - Solid ```tsx // Solid: pages/users/[id]/index.tsx import { Suspense } from "solid-js"; import { createAsync, query, useParams } from "@solidjs/router"; import fetchClients from "_/fetch"; const { GET } = fetchClients["users/[id]"]; // wrap the fetch in query() so preload and createAsync share one cache key - // both call getUser with the same arg and the result is fetched once const getUser = query((id: string) => GET([id]), "user"); // SolidJS Router calls preload on navigation intent; read params from its arg export const preload = ({ params }) => getUser(params.id); export default function UserProfile() { const params = useParams(); const user = createAsync(() => getUser(params.id)); return {user()?.name}; } ``` ### The Same Call in CSR and SSR - Vue ```vue ``` ### The Same Call in CSR and SSR - Svelte ```svelte
{user.name}
``` ### The Same Call in CSR and SSR - MDX ```mdx // MDX: pages/users/[id]/index.mdx import fetchClients from "_/fetch"; import { useLoaderData } from "_/use"; const { GET } = fetchClients["users/[id]"]; export const loader = ({ params }) => GET([params.id]); export const user = () => useLoaderData(); export const User = () => { return
{user()?.name}
; }; ``` ### The Same Call in CSR and SSR (cont.) Types flow through these abstractions - loaders, resources, hooks, and components automatically know the response shape from your API definition. KosmoJS only owns the envelope - the typed fetch client and how requests cross the wire. Everything above it is the framework's own model: Solid's `query`/`createAsync`, React Router's `loader` and `useLoaderData`, Vue's `useLoaderData`, Svelte's `useLoaderData`, MDX's `useLoaderData` - each reads through its native patterns, with no proprietary abstraction layered on top. The client is just a typed function that takes the params array you build and returns a promise; where and how you call it is entirely the framework's. ## These Integrations Work Under SSR Too The client returns standard promises, so nothing above changes when the page is server-rendered. What matters is **when** the fetch fires: loaders and preloaded resources run during the SSR render, so they take the in-process path and their result is reused on hydration rather than refetched. A fetch in `useEffect` / `onMounted` does not run during SSR - it fetches in the browser after hydration, like a plain CSR app. [Isomorphic clients ›](/fetch/isomorphic-clients) ## Suspense Is Your Responsibility Solid's `createAsync` (like `createResource`) suspends: it reports its pending state to the nearest `` boundary and propagates errors to the nearest ``. KosmoJS does not provide either for you - the seeded `App` boilerplate renders its children directly, deliberately not wrapping the app in ``, because one app-wide boundary is an anti-pattern: any pending fetch anywhere collapses the whole page to a single fallback and unrelated async work shares one loading state. Scope the boundary to the data component or a sensible subtree yourself: ```tsx [SolidJS] import { Suspense } from "solid-js"; import { createAsync, useParams } from "@solidjs/router"; export default function UserProfile() { const params = useParams(); const user = createAsync(() => getUser(params.id)); return ( Loading...}>
{user()?.name}
); } ``` React's `loader`/`useLoaderData` resolves before render and does not suspend, so it needs no boundary unless you reach for `React.lazy` or a promise-throwing `use()`. The same holds for Vue, Svelte, and MDX loaders - they resolve before render, so only Solid's `createAsync` needs a boundary in the common case. Wrapping the whole app in one boundary does work if you accept the tradeoff - it is your call, not a default KosmoJS makes for you. See [Data Preloading](/frontend/data-preload#suspense-is-your-responsibility) for the full breakdown. --- --- url: /fetch/isomorphic-clients.md description: >- One fetch client, two transports - a same-origin request in the browser and an in-process dispatch into your API during SSR. What decides which one runs, what the in-process path actually does, how hydration reuses the result, and what happens when an SSR fetch fails. --- A [fetch client](/fetch/intro) is one function you call the same way everywhere: ```ts import fetchClients from "_/fetch"; const { GET } = fetchClients["users/[id]"]; const user = await GET([123]); // a component, a loader, a preload - same call ``` What changes underneath is the **transport**: | Where the call runs | Transport | What it costs | |---|---|---| | Browser | Global `fetch`, same-origin request | A normal HTTP request | | Server, during SSR | Direct dispatch into the API app | A function call and an object - no socket, no localhost hop | There is nothing to configure and no second client to import. ## Nothing is patched Worth saying plainly, because "isomorphic fetch" elsewhere often means monkey-patching: `globalThis.fetch` is never touched - not in the browser, not in Node. The transport is a parameter of the fetch client, not a global that gets swapped: * In the browser no transport is passed, so the client calls the platform's own fetch, pristine - with every redirect, credentials, caching, `AbortSignal` semantics, etc. * On the server the client is constructed with a transport that speaks the same `Request -> Response` contract and hands the request to the API app instead of to the network. ## What decides which transport you get Not a setting - **where and when the call happens**: | Situation | Transport | |---|---| | `pnpm dev`, any folder | Network. The dev server is always [client-rendered](/dev-build-run/development-workflow#dev-renders-client-side), so there is no SSR pass to dispatch in | | Production CSR folder | Network | | SSR folder, call made **during render** - `loader`, `preload`, `createAsync` | In-process | | SSR folder, call made **after hydration** - `useEffect`, `onMounted`, an event handler | Network | | SSG build | In-process (see [below](#ssg-runs-the-same-path-at-build-time)) | The rule underneath is simple: a fetch that fires while the server is rendering the page runs in the server's process; a fetch that fires in the browser runs in the browser. ::: tip The dev server never shows you the in-process path Dev is CSR, so the fetch you are watching in the network tab is the network one - even in a folder with SSR enabled. Run [pnpm preview](/dev-build-run/production-preview) to exercise the real SSR path. ::: ## What the in-process path actually does It is not a shortcut around your API. The SSR build bundles the backend in, and the client hands it a real `Request`: * **Dispatched into the app instance** - `app.fetch(request)` for backends that expose it, an in-memory injection for the Node-style one. Either way the request goes through the whole chain: routing, [global](/backend/middleware) and [cascading](/backend/cascading-middleware) middleware, validation, your handler, [error handling](/backend/error-handling), response shaping. * **With the page request's headers as defaults.** Cookies, `authorization`, tracing headers from the incoming request are forwarded, so an authenticated page renders authenticated data. Anything you set on the call itself wins over a forwarded value. * **Following redirects in-process** - up to the five hops the fetch spec allows, including the `303` (and `301`/`302` from `POST`) rewrite to `GET`. * **Without patching anything global.** Only fetch clients switch transports. Every other `fetch` in your app - a call to a third-party API, say - behaves exactly as it always did. ## Client-side validation is skipped under SSR The client's pre-flight check exists to save a round trip. During SSR there is no round trip, so it is disabled automatically and validation runs on the API endpoint only. [Details ›](/fetch/validation) ## Hydration does not refetch A request made during SSR is not repeated in the browser: * **React and Solid** reuse the result through their built-in hydration. * **Vue, Svelte and MDX** reuse it through the loader: the result is serialized into the page during SSR and read on the client before the loader would run. So the common pattern - a loader that fetches, a component that renders it - costs exactly one call, made in the server's process, and the browser starts from the finished result. ## When an SSR fetch fails A failing call during SSR is not the same event as a failing call in the browser, and it is worth knowing what the server does with it. The in-process transport throws on a non-2xx response, and the error is also stashed on the request-scoped store - because some frameworks swallow a rejected loader and render a partial tree anyway, which would otherwise produce a half-rendered page with no visible error. When the render finishes and that error is present, **the SSR output is discarded and the client shell is served instead**. The browser then renders the page itself, where the failure reaches your own [error boundaries](/frontend/error-boundaries) and surfaces the same way it would in a CSR app. The server logs it: ```txt WARN: SSR failed, fallback to CSR SSRFetchError: /api/users/123: 500 [ Internal Server Error ] ``` That recovery needs an untouched response, so it applies to **string-rendered routes** - the default. A [streamed route](/frontend/server-side-render#streaming-routes-do-not-recover) has already flushed its status line and opening HTML by the time a fetch fails, so there is nothing left to replace: streaming routes must handle fetch failures in the page itself. The fallback is a deliberate trade - one consistent place to handle errors instead of server-side boundaries that behave differently in every framework - but it has a practical consequence worth internalising: ::: warning A page that renders fine can still have lost its SSR If a page silently arrives as an empty shell in production, check the server log before checking the client. A single failing API call during render is enough to drop the whole page back to CSR, and the page still works - it just stopped being server-rendered. ::: To catch that without reading logs, give the renderers an [onError hook](/frontend/server-side-render#onerror-hook) - it is called with the error that ended the render, so a lost SSR becomes an event in your monitoring rather than a line on stdout. It reports only; the fallback happens either way. This is a **serving-time** trade, and it applies to SSR only: at build time there is no visitor waiting, so [SSG](#ssg-runs-the-same-path-at-build-time) writes nothing and fails the build instead. Fetch clients always throw on failure, on both sides - but not the same object. In the browser the thrown error carries the `response` and the parsed error `body`; during SSR the transport throws first, with the route and status in its message. And a `ValidationError` can only ever come from the browser, since the pre-flight check is not there under SSR. [Details ›](/fetch/error-handling) ## SSG runs the same path at build time [Static generation](/frontend/static-site-generation) is not a third mode. The build starts a disposable SSR server, requests each route from it, and writes the returned HTML to disk - so every fetch made during those renders takes the in-process path, inside the build. Two consequences: * **Your API runs during the build**, so the build needs the access production has - the real database, the CMS, whatever your routes read. That is a CI concern rather than a laptop one: the usual shape is a workflow that ships the sources to the production environment - or to a runner with the same credentials and network reach - and builds there, so every page is rendered against exactly the data production would have served. * **A failed fetch costs you the whole build.** SSG does not fall back to a client shell: pre-rendering goes through every path, and if any of them failed it writes nothing and throws the collected errors. [Details ›](/frontend/static-site-generation#error-handling) ## What does not change * **`path` and `href`** always produce URL strings - they are for links, redirects and external references, not requests, so there is nothing to swap. [Details ›](/fetch/utilities) * **Types.** The same `ResponseT` and parameter types apply on both sides; nothing about the call signature depends on where it runs. * **Your framework's data model.** Loaders, `createAsync`, `useLoaderData` - the client is a plain promise-returning function and the framework owns everything above it. [Details ›](/fetch/integration) *** [Read more](/essentials/why-http) on why the boundary is there and why keeping it does not have to cost a round trip. --- --- 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: /fetch/type-safety.md description: >- Fetch clients are fully typed - parameters, payload, and response all mirror the backend route. Access response types on the client with the ResponseT map. --- The fetch client mirrors your backend route exactly. Parameters, payload, and response are all typed from the same route definition - change the route, and every call site updates with it. Parameters and payload are checked the moment you call a method: the signature expects the parameter array and payload shape your route declares, and passing the wrong thing is a compile error. Nothing to import, nothing to annotate. ```ts [pages/example/index.tsx] import fetchClients from "_/fetch"; const { GET } = fetchClients["users/[id]"]; await GET([123]); // ✅ number param, as declared await GET(["abc"]); // ❌ type error - param must be a number await GET([123], { query: { q: 1 } }); // ❌ type error - `q` isn't in the query type ``` ## Response Types The response is fully typed at the call site too - awaiting `GET(...)` gives you a value typed to that route's response, no annotation needed. So most of the time you never name the response type at all; you just use the result. What you occasionally need is the response type *out of band* - away from the call, where there's no awaited result to infer from. Typing a Solid `createAsync` accessor, a `useLoaderData()` result, a store, a component prop, or a shared helper are the common cases. For those, KosmoJS derives a `ResponseT` map, exported from `_/fetch`. `ResponseT` is keyed by route name, then request method, and resolves to the response **body** type declared on that handler - the same type awaiting the method gives you, available as a name you can reference anywhere. Given this backend 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 response type is available on the client as: ```ts [pages/example/index.tsx] import f, { type ResponseT } from "_/fetch"; ResponseT["users/[id]"]["GET"]; // { id: number; name: string; email: string } ``` ::: tip Response types are opt-in An entry exists in `ResponseT` only when the handler declares a `response`. If a route defines no response type, it has no `ResponseT` entry - just as it has no response validation. Declare a `response` to get both. ::: ### Without a `response`, the result is `unknown` This is worth being explicit about, because it is the one case where the client is *not* typed for you. A method whose handler declares no `response` returns `Promise`: ```ts // api/users/index.ts - no `response` declared export default defineRoute<"users">(({ GET }) => [ GET<{ query: { page?: number }, // no response schema // [!code hl] }>(async (ctx) => { // ... }), ]); ``` ```ts const users = await fetchClients["users"].GET(); users.length; // ❌ 'users' is of type 'unknown' ``` `unknown` is deliberate rather than `any`: nothing was declared, so nothing is claimed, and TypeScript makes you say what you expect instead of silently trusting it. Declaring `response` on the handler is what fixes it - and it is the same one line that turns on response validation and the OpenAPI response schema: ```ts GET<{ response: [200, "json", Array] // [!code hl] }>(async (ctx) => { /* ... */ }) ``` ```ts const users = await fetchClients["users"].GET(); users.length; // ✅ Array ``` So the practical rule: **declare `response` on any handler whose result the frontend consumes.** Params and payload are typed from the route definition regardless - it is only the return value that depends on `response`. ## Multiple Responses A handler can declare a union of responses - different status codes returning different bodies. `ResponseT` follows, collapsing to a union of the body types: ```ts [api/users/index.ts] export default defineRoute<"users">(({ POST }) => [ POST<{ json: NewUser, response: | [201, "json", User] // created - full record | [202, "json", { queued: true }] // accepted for async processing | [409] // conflict, no body }>(async (ctx) => { // response must comply with one of the defined schemas }), ]); ``` Variants without a body - no third tuple element, like the bare `[409]` above - contribute nothing to the type, so they drop out of the union: ```ts [pages/example/index.tsx] import f, { type ResponseT } from "_/fetch"; ResponseT["users"]["POST"]; // User | { queued: true } ``` ## Using Response Types The out-of-band cases in practice - the hook that needs annotating at its untyped boundary - and a shared helper naming the type once. `helpers.ts` is the same file for every framework. ### Using Response Types - React ```tsx // React: pages/users/[id]/index.tsx import { useLoaderData } from "react-router"; import f from "_/fetch"; import { formatUser, type User } from "./helpers"; const { GET } = f["users/[id]"]; export const loader = ({ params }) => GET([params.id]); export default function UserProfile() { // useLoaderData is untyped at the boundary - annotate it with User type const user = useLoaderData(); return
{formatUser(user)}
; } ``` ### Using Response Types - Solid ```tsx // Solid: pages/users/[id]/index.tsx import { Show, Suspense } from "solid-js"; import { createAsync, query, useParams } from "@solidjs/router"; import f from "_/fetch"; import { formatUser, type User } from "./helpers"; const { GET } = f["users/[id]"]; // createAsync infers from the fetcher, so there is no boundary to annotate - // User earns its keep on the query wrapper and on helper signatures const getUser = query((id: string): Promise => GET([id]), "user"); export const preload = ({ params }) => getUser(params.id); export default function UserProfile() { const params = useParams(); const user = createAsync(() => getUser(params.id)); return ( {(u) =>
{formatUser(u())}
}
); } ``` ### Using Response Types - Vue ```vue ``` ### Using Response Types - Svelte ```svelte {#if user}
{formatUser(user)}
{/if} ``` ### Using Response Types - ./helpers.ts ```ts // helpers.ts - framework-agnostic, identical in every folder import type { ResponseT } from "_/fetch"; // name the type once, reuse it across components export type User = ResponseT["users/[id]"]["GET"]; export const formatUser = (user: User) => { return `${user.name} [${user.email}]`; } ``` ### Using Response Types (cont.) > MDX is absent here on purpose: `.mdx` is not TypeScript, > so it has no type arguments and no type imports - an MDX page reads `useLoaderData()` untyped. The type flows from the backend route through the client to wherever you consume it. Change the route's `response` shape and every consumer - loaders, resources, helpers, props - updates with it, surfacing mismatches at compile time instead of in production. See [Fetch Client Integration](/fetch/integration) for the full set of framework patterns, and [Response Validation](/validation/response) for declaring the `response` schema on the backend. --- --- url: /fetch/error-handling.md description: >- Fetch clients always throw on failure, so every failure is catchable. Distinguish ValidationError from network and server errors, and handle them with a try-catch or an error boundary at layout or app level. --- Fetch clients always **throw on failure**. Every failure - a validation error before the request, an HTTP error status, or a network/transport failure - surfaces as a thrown error you can catch. The return type stays the response type; a failed call never resolves to `undefined`, and nothing is silently swallowed. ## Catching Errors Wrap a call in `try/catch` and branch on the error type: ```ts [pages/example/index.tsx] import fetchClients, { ValidationError } from "_/fetch"; const useFetch = fetchClients["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.errorMessage); } else { // network error, or an HTTP error status from the server console.error("Request failed:", error); } } ``` Three failure kinds reach that `catch`: * **`ValidationError`** - parameters or payload failed client-side validation. Thrown before any network request, so nothing was sent. Carries the same structured detail as a server-side `ValidationError`, so you can surface field-level feedback immediately. [Details ›](/validation/error-handling) * **HTTP error status** - the request was sent and the server responded with a non-2xx status. The thrown error carries the response and the parsed error body. * **Network / transport failure** - the request never completed (DNS failure, connection refused, server down). The original transport error is thrown. Every failure throws, so a plain `try/catch` around the call catches it. Nothing is swallowed. ## Error Boundaries For anything beyond a single call, you don't want a `try/catch` per call - you want one place that catches failures for a whole subtree. That is an **error boundary**, mounted at layout level. Because the client throws naturally, a fetch failure propagates like any other thrown error and the nearest boundary catches it - so one boundary covers every call rendered beneath it. Each framework provides its own boundary; mount it where you want failures to be caught (usually the layout, so it wraps every page): * **React** - an error boundary component (a class boundary or `react-error-boundary`). * **Solid** - `` from `solid-js` around the layout's content. * **Vue** - a wrapper using `onErrorCaptured`, or `app.config.errorHandler` globally. * **Svelte** - `` where available, otherwise handle at the data/load layer. * **MDX** - renders through Preact, so it uses a Preact boundary (`useErrorBoundary` from `preact/hooks`). A layout-level boundary for each framework: ### Error Boundaries - React ```tsx // React: pages/dashboard/layout.tsx import { ErrorBoundary } from "react-error-boundary"; export default function Layout({ children }: { children: React.ReactNode }) { return ( (

Something went wrong: {error.message}

)} > {children}
); } ``` ### Error Boundaries - Solid ```tsx // Solid: pages/dashboard/layout.tsx import { ErrorBoundary, type ParentProps } from "solid-js"; export default function Layout(props: ParentProps) { return ( (

Something went wrong: {String(error.message ?? error)}

)} > {props.children}
); } ``` ### Error Boundaries - Vue ```vue ``` ### Error Boundaries - Svelte ```svelte {@render children()} {#snippet failed(error, reset)}

Something went wrong: {error instanceof Error ? error.message : String(error)}

{/snippet}
``` ### Error Boundaries - MDX ```tsx // MDX: pages/dashboard/layout.mdx import { useErrorBoundary } from "preact/hooks"; import type { ComponentChildren } from "preact"; // MDX pages render through Preact; wrap {props.children} in your layout with this. export default function ErrorBoundary(props: { children: ComponentChildren }) { const [error, reset] = useErrorBoundary(); if (error) { return (

Something went wrong: {error instanceof Error ? error.message : String(error)}

); } return props.children; } ``` ### Error Boundaries (cont.) A boundary catches errors thrown **during render**. The two paths below are about getting each kind of failure onto that render path. ## Where Errors Surface ### TanStack Query When a call runs inside `createQuery` / `useQuery`, TanStack catches the exception and stores it in `query.error` - it does **not** throw during render by default, so no boundary fires. Read `query.error` to handle it inline, or set `throwOnError` to escalate the error into the nearest boundary: ```ts const query = createQuery(() => ({ queryKey: ["user", id()], queryFn: () => fetchClients["users/[id]"].GET([id()]), throwOnError: true, // let the error boundary catch it instead of query.error })); ``` Use `query.error` for inline, per-widget error UI; use `throwOnError` when a failed load should hand off to the same layout-level boundary as everything else. ### Route loaders A fetch call inside a route `loader` (React Router) or `preload` (Solid) reaches the framework's own route-error channel: * *React Router* surfaces the error through the route's `errorElement` (read it with `useRouteError`), rendered the same on the server render and after hydration. * *Solid* surfaces it through the resource so a downstream `` under `` catches it. Handle loader errors with the framework's route-level error UI, not with a `try/catch` inside the loader - catching inside the loader hides the failure from the machinery designed to render it. ### Event handlers and mutations A fetch call in a click or submit handler (including a TanStack mutation) runs outside render, so a render boundary can't see it. Handle these with a local `try/catch` where the call is made, and read `mutation.error` for mutation state. ## Defense in Depth Client-side validation catches bad input before a request is sent, but it is a UX convenience, not a security boundary. The server always re-validates with the same schemas. Treat client validation as fast feedback and the server's `ValidationError` response as the authority. [Details ›](/fetch/validation) --- --- 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: /dev-build-run/development-workflow.md description: >- The KosmoJS dev server - HMR for client code, hot reload for the API, custom request 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. One dev server covers both sides of a folder. Client modules go through `Vite` with `HMR`; API routes run in the same process and hot-reload on change. There is no second command to start and no proxy to configure - requests are dispatched between the two by path. ## 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. Blank route and page files are seeded. 2. `Vite` compiles `api/app.ts` 3. Dev server starts, serving both client pages and your API routes 4. Requests are routed between Vite and your API by the folder's `backend.base` 5. File watchers monitor client modules and API files for changes ## Dev Renders Client-Side `pnpm dev` is always `Vite` + `HMR` + client-side rendering, even when folder has [SSR enabled](/frontend/server-side-render). Server rendering happens in the production build, so there is no server-rendered markup to look at while the dev server is running. This catches people out coming from Next/Nuxt/TanStack Start, where dev mirrors production rendering. To see, test or debug anything server-rendered, run [kosmo preview](/dev-build-run/production-preview). It serves the real production build and still reloads when you edit a file. ## Hot Reload vs HMR The two sides of a source folder reload differently: * **Client**: HMR - changed modules are patched in place, component state survives the edit. A component, a style, a page - the browser updates without a navigation. * **API**: hot reload - on change, the API *program* restarts as a whole. There is no HMR for the backend, and reloads fire on more than route edits (config changes, shared types etc.) A full restart means module-level state resets on every reload. This is by design, not a limitation to work around: a backend should be stateless, in development for fast reliable reloads and in production so it can restart, scale, and run as multiple instances. Keep anything that must survive a restart in a real store from day one - the dev reload cycle is simply an early rehearsal of what production restarts do anyway. What *does* need care across reloads is resources: open connections leak when the program restarts around them. Close them in the [teardownHandler](#teardownhandler) hook. ## api/dev.ts `api/dev.ts` is where the API side of the dev server is wired up. It is re-evaluated on every relevant change: the previous setup is torn down, the file re-imported, and the fresh handler serves the next request. The three hooks below sit on that cycle. ### requestHandler Returns the API request handler. Seeded default: #### requestHandler - Hono ```ts // Hono: api/dev.ts import { getRequestListener } from "@hono/node-server"; import { devSetup } from "_/api:factory"; import app from "./app"; export default devSetup({ requestHandler() { return getRequestListener(app.fetch); }, }); ``` #### requestHandler - H3 ```ts // H3: api/dev.ts import { toNodeHandler } from "h3/node"; import { devSetup } from "_/api:factory"; import app from "./app"; export default devSetup({ requestHandler() { return toNodeHandler(app); }, }); ``` #### requestHandler - Koa ```ts // Koa: api/dev.ts import { devSetup } from "_/api:factory"; import app from "./app"; export default devSetup({ requestHandler() { return app.callback(); }, }); ``` #### requestHandler (cont.) Override this for custom routing logic - WebSocket handling, multi-handler dispatch, etc. ### requestMatcher Controls which requests go to your API vs Vite. By default, requests are routed to the API handler if their URL starts with `backend.base` or match any `backend.alias`. Use this to implement custom heuristics for detecting API requests: ```ts export default devSetup({ requestHandler() { // ... }, 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() { // ... }, async teardownHandler() { if (dbConnection) { await dbConnection.close(); dbConnection = undefined; } }, }); ``` Without cleanup, frequent rebuilds during active development can exhaust database connections. ::: tip This hook is dev-only. In production the process exits and the OS reclaims everything; it exists because a dev reload restarts the API *inside* a long-running process. ::: ## Inspecting API Routes Routes can be inspected by providing `debug` option to `appFactory` in `api/app.ts` (omitted by default, feel free to add it as needed): ```ts [api/app.ts] import appFactory, { routes } from "_/api:factory"; import defaultErrorHandler from "./errors"; export default appFactory( routes, { debug: true }, // [!code ++] ({ app }) => { // ... }) ``` Example output: ```txt /api [ index/index.ts ] methods: GET|HEAD middleware: slot: @extendContext useExtendContext slot: validate:params useValidateParams handler: indexHandler ``` > 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: `headline`, `methods`, `middleware`, `handler`. Use this to display only headline: ```ts export default appFactory(routes, { debug: "headline" }, ({ app }) => { // ... }) ``` If you rather need a custom logger, provide a function instead; it will be provided with full debug object and the route itself. ```ts [api/app.ts] export default appFactory( routes, { debug(log, route) { console.log(log.full); }, }, ({ app }) => { // ... }, ); ``` The `log` signature: ```ts { headline: string; methods: string; middleware: string; handler: string; full: string; } ``` --- --- url: /dev-build-run/production-preview.md description: >- Run the real KosmoJS production build locally with kosmo preview - server-rendered pages, bundled assets and production validation policy, still reloading on change. --- The dev server is fast: `HMR`, client-side rendering. Most of the time that is what you want. Sometimes it is not. Server-rendered markup, a bundling or tree-shaking problem, asset hashing, the production validation policy - none of these exist until you build, and none of them are visible from the dev server. `preview` builds the project and runs [dist/run.js](/dev-build-run/building-for-production#one-entry-point-for-the-whole-project) - the same entry point production starts. ```sh pnpm preview # all source folders pnpm preview front # specific folder ``` Then it watches your sources, and on change it rebuilds. Production output, development loop. Default port is `4558`, configured as `previewPort` in `package.json`. It is deliberately separate from `devPort`, so preview and the dev server can run side by side and you can compare the two in adjacent tabs. ## What You Are Actually Looking At Everything comes from `dist/`. Nothing is transformed on the fly: * **`SSR` folders** render on the server, so you see real server-rendered markup and can watch hydration happen. * **`CSR` folders** are served as built static assets - hashed filenames, real chunk splitting, the `index.html` a static host would serve. * **`API` folders** answer from the bundled backend, with [response validation off](/validation/response#development-vs-production) unless you asked for it per handler - the production policy, not the dev one. If a page works in preview, it works when deployed. That is the whole point of the command: the gap between "works locally" and "works in production" is where `SSR` bugs live. ## Hot Reload, Not HMR A change triggers a full rebuild. There is no module patching and no preserved state. This is not a missing feature. `HMR` works by replacing modules in a running graph, which is exactly what a production bundle does not have. Preview's contract is that the page in the browser is the page production serves; patching it in place would break that contract, and a preview you cannot trust is worse than no preview. A rebuild is a full production build, so expect seconds rather than the milliseconds of `HMR`. Use the dev server for iteration and preview to verify. ::: tip A failed rebuild leaves the previous build serving. The error is printed to the terminal and the page keeps working, so a typo mid-edit does not take the preview down. ::: ## When to Reach for It * Checking server-rendered output, or debugging a hydration mismatch * Confirming something works after bundling - dynamic imports, tree-shaking, anything that behaves differently unbundled * Verifying assets, `base` paths and deep links resolve when the app is not at the root * Sanity-checking a multi-folder layout end to end, since preview dispatches across folders exactly as `dist/run.js` will in production * Reproducing a bug that only appears in a deployed build For everyday work, keep using [pnpm dev](/dev-build-run/development-workflow). Preview is the check before you ship. --- --- url: /dev-build-run/building-for-production.md description: >- Build and deploy KosmoJS applications to production - a single dist/run.js entry point across all source folders, plus per-folder servers for containers, serverless, and edge runtimes. --- Each source folder builds independently, into its own subdirectory of `distDir` - a client bundle, an API bundle when it has a backend, and an SSR bundle when SSR is on. Alongside them the build writes one entry point that serves every folder at once, so the simple deployment is a single process even when the project has several folders. Splitting them across hosts stays available: each folder's bundles are runnable on their own. ## One Entry Point for the Whole Project The build writes `dist/run.js` - a dispatcher over every source folder in the project. Start it and the whole application is up: ```sh node dist/run.js -p 4556 ``` It routes incoming requests by path the same way the dev server does: each folder's `frontend.base` and `backend.base` decide what it owns, longest prefix first. A project with a marketing site at `/`, an admin app at `/admin` and an API-only folder at `/svc` is one process, one port, no reverse proxy needed to glue the folders together. Each folder is served as what it is. `SSR` folders render on the server; `CSR` folders are served as static assets with a fallback to their `index.html`; `API`-only folders answer on their `backend.base`. You do not configure any of this - it follows from the folder's config. `dist/run.js` is also what [kosmo preview](/dev-build-run/production-preview) runs, so what you check locally is the same entry point production serve. ### Other Runtimes `dist/run.js` is built on `node:http`, which `Bun` and `Deno` both implement, so the same file runs unchanged on all three: #### Other Runtimes - Node ```sh node dist/run.js -p 4556 ``` #### Other Runtimes - Bun ```sh bun dist/run.js -p 4556 ``` #### Other Runtimes - Deno ```sh deno run -A dist/run.js -p 4556 ``` #### Other Runtimes (cont.) This is worth stressing because it is easy to assume otherwise: nothing in the dispatcher is tied to a runtime-specific server API. It is `node:http` all the way down, and the compatibility layers in `Bun` and `Deno` cover it. ## Running Folders Separately `dist/run.js` is the simple path, not the only one. Deploy folders separately when they have separate lifecycles - a marketing site on a CDN and an API in a container, an admin app scaled independently of the public one, or a folder that belongs in a different region entirely. The per-folder servers below are what `dist/run.js` dispatches to internally, so nothing changes about how a folder behaves when you run it on its own. ## Build Output ```txt dist/ ├── run.js # dispatcher over every folder └── front ├── api │ ├── app.js # app instance │ └── server.js # ready-to-use API server ├── client │ ├── assets/ # scripts, styles, images │ └── index.html └── ssr ├── app.js # SSR app instance └── server.js # SSR server bundle ``` The SSR output is only present when [SSR is enabled](/frontend/server-side-render). ## What to Deploy When running folders separately, what you deploy depends on how the folder renders. (With `dist/run.js` this is all handled for you - the section below matters when you split folders across hosts.) ### SSR folders - deploy the SSR server only. The build bundles the folder's backend *into* the SSR server: one process serves the client assets, renders pages, and answers API requests. During server-side rendering, the [isomorphic fetch client](/fetch/isomorphic-clients) calls your route handlers in-process - no network hop, no localhost round-trip. After hydration, the same server answers the browser's API calls over HTTP through a built-in gateway that routes everything under the folder's `backend.base` to the bundled backend. ```sh node dist/front/ssr/server.js -p 4556 # pages + API, one process ``` There is nothing else to run - `dist/front/api/server.js` exists in the build output, but deploying it alongside the SSR server would just duplicate the API. ### CSR folders - deploy the API server, serve the client statically. The browser is the only consumer of your API here, and it reaches it over the network: run `dist/front/api/server.js` and put `dist/front/client/` behind any static host or CDN. ### API-only folders (no frontend) Deploy the API server, same as CSR minus the static assets. ## Running the API server For CSR and API-only setups, the simplest deployment is running the bundled server directly: ### Running the API server - Node ```sh node dist/front/api/server.js -p 4556 ``` ### Running the API server - Bun ```sh bun dist/front/api/server.js -p 4556 ``` ### Running the API server - Deno ```sh deno run -A dist/front/api/server.js -p 4556 ``` ### Running the API server (cont.) For more control, use the app factory at `dist/*/api/app.js`. **Hono / H3** - `app.fetch` is a Web Fetch API handler, so it plugs into each runtime's native server directly: ### Running the API server - Hono on Node ```js 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); ``` ### Running the API server - H3 on Node ```js import { createServer } from "node:http"; import { toNodeHandler } from "h3/node"; import app from "./dist/front/api/app.js"; createServer(toNodeHandler(app)).listen(3000); ``` ### Running the API server - Deno ```ts import app from "./dist/front/api/app.js"; Deno.serve({ port: 3000 }, app.fetch); ``` ### Running the API server - Bun ```ts import app from "./dist/front/api/app.js"; Bun.serve({ port: 3000, fetch: app.fetch }); ``` ### Running the API server (cont.) **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 [Koa on Node / Bun / Deno ~vscode-icons:file-type-js~] import { createServer } from "node:http"; import app from "./dist/front/api/app.js"; createServer(app.callback()).listen(3000); ``` Or use `app.listen()` directly for Node only support: ```js [Koa on Node ~vscode-icons:file-type-js~] import app from "./dist/front/api/app.js"; app.listen(3000); ``` --- --- url: /openapi.md description: >- Automatically derive OpenAPI 3.1 specifications from KosmoJS API routes. Analyzes route structure, TypeScript types, and validation schemas to produce standards-compliant documentation. --- KosmoJS derives 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 OpenAPI Simply add it to your source folder's `kosmo.config.ts`: ```ts import { defineConfig } from "@kosmojs/dev"; export default defineConfig({ backend: { stack: "hono", base: "/api", openapi: { // [!code ++:6] outfile: "openapi.json", openapi: "3.1.0", info: { title: "My API", version: "1.0.0" }, servers: [{ url: "https://api.example.com/api" }], }, }, }); ``` ## Configuration `backend.openapi` is the one block whose options are mandatory. For how it sits alongside the rest of the folder config, see the [configuration reference](/essentials/config#backend-openapi). ### 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) - URL the API is served from, including the `backend.base` prefix. Paths in the spec are relative to this, so getting it wrong is the usual cause of a spec whose endpoints 404 - see [Server URLs and Route Paths](#server-urls-and-route-paths) * `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", }, }, // this folder has backend.base "/api", so the dev server carries that prefix - // in production the API is deployed at the root of its own host and carries none. servers: [ { url: "http://localhost:4556/api", description: "Development server" }, { url: "https://staging-api.myapp.com", description: "Staging environment" }, { url: "https://api.myapp.com", description: "Production server" }, ], }; ``` ### Server URLs and Route Paths Paths in the spec are route names, exactly as they appear under `api/`. A route at `api/users/[id]/index.ts` becomes `/users/{id}`, and the `index` route becomes `/` - neither carries the folder's [backend.base](/essentials/config#backend-base-required). That is deliberate, not an omission. In `OpenAPI`, paths are relative to `servers`, and the prefix an API answers on is a deployment decision rather than a property of the route. The same backend may sit behind `/api` in development, at the root of a dedicated host in production, and under `/v2/api` behind a gateway - so the prefix belongs to the server entry, and the paths stay the same in all three. This is why `servers` is mandatory: it is the only place the prefix is recorded. Give each entry the full prefix, origin plus `backend.base`: ```ts // folder with backend.base "/api" servers: [ { url: "http://localhost:4556/api", description: "Development server" }, // deployed at the root of its own host - no prefix to add { url: "https://api.myapp.com", description: "Production server" }, ]; ``` ```ts // folder with backend.base "/admin/api" servers: [ { url: "http://localhost:4556/admin/api", description: "Development server" }, { url: "https://myapp.com/admin/api", description: "Production server" }, ]; ``` A client built from the spec resolves `/users/{id}` against whichever server it is pointed at, requesting `http://localhost:4556/api/users/42` in development and `https://api.myapp.com/users/42` in production - one spec, no per-environment rebuilds. ::: tip If **Try it out** in `Swagger UI` returns `404`, check the server URL first. A missing `backend.base` prefix is the usual cause. ::: ## Derived 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 produce multiple paths. For a route at `users/[id]/posts/{postId}/index.ts`, the spec contains: * `/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 Updates The spec is recomputed automatically whenever you modify route definitions, types, or validation schemas. This happens in the background alongside validation and fetch derivation - no manual rebuild step required. Serve the 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: /faq.md description: Frequently asked questions about KosmoJS for developers and LLM agents --- **KosmoJS FAQ** ### Getting Started & Project Setup #### What is KosmoJS and what problem does it solve? A composable meta-framework for organizing multiple apps in a scalable project. It avoids the friction of microservices (drifting shared types, separate CI/deploy), monorepos (workspace/package/build-cache overhead and a `packages/shared` dumping ground), and DIY glue (hand-rolled scripts that become load-bearing). Instead it uses a Vite-inspired "source folders" approach: 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 control of backend, frontend, state, styling, database, and deploy target; KosmoJS owns routing conventions, the validation pipeline, middleware composition, dev workflow, and build orchestration. [Details ›](/about) #### Is KosmoJS a runtime, a bundler, or a framework? Rather a meta-framework built on top of Vite. There is no proprietary runtime, no custom bundler, and no framework lock-in - every layer (Vite, Hono/H3/Koa, React/Vue/Solid/Svelte/MDX) is a tool you can use, debug, and replace independently. [Details ›](/about) #### How do I create a new KosmoJS project? Run `npm create kosmo demo` (or `pnpm create kosmo demo` / `yarn create kosmo demo`). An interactive setup creates the project together with your first source folder, asking only for the framework and backend - the folder defaults to `app` at base `/`. Then `cd ./demo` and install dependencies. Use `.` as the name to bootstrap into the current folder (e.g. a freshly cloned repo). [Details ›](/start) #### How do I create a project non-interactively? Choose the framework and backend up front - when flags are present no prompts appear: `npm create kosmo demo -- --frontend react --backend hono` (pnpm and yarn forward flags without the extra `--`: `pnpm create kosmo demo --frontend ...`). Required: `--frontend ` or `--no-frontend`, and `--backend ` or `--no-backend` - the choice is always explicit; a missing flag is an error, never a silent default. Optional: `--ssr`, `--ssg`, `--tsq`, `--overwrite`. The first folder is always `app`, with pages at `/` and its API at `/api`. [Full flag reference ›](/cli/create#cli-mode) Use `.` as the project name to scaffold into the current folder: `npm create kosmo . -- --frontend ...` [Details ›](/start) #### How do I add a source folder? Run `npm run folder` (or `pnpm folder` / `yarn folder`) - interactively, or with flags. [Details ›](/cli/folder) #### What am I prompted for when adding a source folder? Frontend, backend, SSR, SSG (only if SSR is on) and TanStack Query. Non-interactive: the name is a positional, plus `--frontend solid|react|vue|svelte|mdx` or `--no-frontend`, `--backend hono|h3|koa` or `--no-backend`, `--ssr`, `--ssg`, `--tsq`, `--overwrite`. Frontend and backend each require a value or its negation flag, and the name is required here - unlike at project creation, it has no default. The folder gets pages at `/` and its API at `//api`. [Details ›](/cli/folder) #### How do I create a backend-only (API) folder, or a frontend-only folder? A source folder doesn't have to ship both sides - framework and backend are independent, and each is optional. In interactive mode, choose `None (API-only folder)` in the framework select, or `None (client-only folder)` in the backend select. In non-interactive mode, pass the matching negation flag for the side you skip: ```sh pnpm folder api --backend hono --no-frontend # backend-only, no UI pnpm folder docs --frontend mdx --no-backend # frontend-only, no backend ``` The seeded `kosmo.config.ts` contains only the block that side needs. [Details ›](/cli/folder) #### Can I create a project without a source folder? No - every project starts with its first source folder; omitting the name positional just falls back to the default (`app`, pages at `/`, API at `/api`). A project without folders has nothing to serve or build - the source folder is the unit of everything in KosmoJS. Add more folders any time with `npm run folder`. [Details ›](/start) #### Why install again after adding a source folder? Adding a folder pulls in framework-specific dependencies that need to be installed. [Details ›](/start) #### How do I start the dev server and what's the default port? `pnpm dev` (all folders) or `pnpm dev front` (one folder). Default port is `4556`. [Details ›](/dev-build-run/development-workflow#starting-the-dev-server) #### How do I change the dev port? It's the `devPort` value in `package.json`. [Details ›](/dev-build-run/development-workflow#starting-the-dev-server) #### How does this compare to Next.js / Nuxt / SolidStart / tRPC / a hand-rolled Vite setup? Unlike Next/Nuxt/SolidStart it doesn't choose your frontend or own your deploy model; unlike tRPC it's route-based (not procedure-based) and also derives OpenAPI and runtime validators; unlike a hand-rolled Vite setup it provides directory routing for both sides, derived validation/clients, an isomorphic fetch client (in-process on the server during SSR, no network hop), out-of-the-box SSR with an opt-in streaming render mode, and multi-folder build orchestration without the DIY glue. [Details ›](/features) ### Source Folders #### What is a source folder? A self-contained app inside the project with its own framework stack, base URL, routing, middleware, layouts, config, and build output - but sharing one `package.json`, one `node_modules`, one database layer, and one set of types. Example layout: * `src/app` (React + Hono, base `/`) * `src/admin` (Vue + H3, base `/admin`) * `src/marketing` (MDX, no backend, base `/`) [Details ›](/features) #### How is this different from a monorepo package / microservices / DIY glue? Folders are not separate packages: no workspaces, no package boundaries, no internal dependency graph, no publishing, no versioning, no workspace protocols. You get monorepo-like structure and microservice-like independence with single-project simplicity. [Details ›](/about) #### Can different folders use different frameworks/backends at the same time? Yes. Each folder picks its own backend (Hono/H3/Koa) and frontend (React/Vue/SolidJS/Svelte/MDX), and they coexist in one project. [Details ›](/features) #### How do folders share types without publishing/versioning? Import a type directly across folders through the reserved aliases - `@/*` for root-level imports, `~/*` for source-folder imports, `_/*` for derived code. Change a database model and every folder sees it immediately. No publishing, no workspace protocols. [Details ›](/essentials/project-structure#path-mappings) #### Can I build/deploy a single folder? Yes - `pnpm build front` builds just that folder. Folders develop together as one project, but can be built all at once or one at a time, and each build is a self-contained entity you can deploy independently. [Details ›](/dev-build-run/building-for-production) #### How do I run/build all folders vs one? `pnpm dev` / `pnpm build` for all; append a folder name (`pnpm dev front`, `pnpm build admin`) for one. [Details ›](/dev-build-run/development-workflow#starting-the-dev-server) #### Do routes/types leak between folders? No - derived types and utilities are scoped per folder. The admin dashboard's navigation types won't include the main app's routes, and vice versa. [Details ›](/essentials/project-structure#path-mappings) #### When should I split into separate folders? One folder per distinct concern (main app, admin, marketing). A useful rule for SSR vs CSR: deploy an SSR folder for marketing content and a CSR folder for the app rather than mixing SSR/CSR within one folder. [Details ›](/frontend/server-side-render#technical-considerations) ### Configuration #### Where does configuration live, and is there a `vite.config.ts`? Per source folder, in `src//kosmo.config.ts` - there is no project-wide kosmo config and **no separate `vite.config.ts`**. Vite's `UserConfig` goes in `viteConfig`, on the `frontend` and `backend` blocks separately - `plugins`, `resolve`, `css`, `define`, and the rest. A few Vite keys are excluded because KosmoJS derives them from the folder layout: `root`, `base`, `cacheDir`, `mode`, `builder`, `future`, `legacy`. Project-wide settings (`distDir`, `devPort`, `previewPort`, scripts) live in the root `package.json`. [Details ›](/essentials/config) #### What options does a source folder config take? Three optional blocks: `frontend`, `backend`, `validation`. `frontend` takes `stack`, `base` (required), `fetch`, `ssr`, `ssg`, `tanstack`, `templates` and `viteConfig`. `backend` takes `stack`, `base` (required), `openapi`, `alias`, `templates` and `viteConfig`. `stack` is either a name or `{ name, plugin }`, where `plugin` is a Vite plugin instance you construct. `validation` is `true` or a TypeBox options object. [Details ›](/essentials/config#the-shape) #### What do I import in `kosmo.config.ts`? `defineConfig` from `@kosmojs/dev`, and nothing else. You name a `stack` and flip features on; `defineConfig` assembles the generators for you. `@kosmojs/dev` also exports the generators themselves, for the `generator` escape hatch that swaps a built-in generator for your own on any block. [Details ›](/essentials/config#bringing-your-own-generator) #### What runs, and in what order? Fixed, and independent of how you write the config: `core -> backend -> validation -> openapi -> fetch -> frontend -> ssr -> ssg`. `coreGenerator` always runs first and is never listed. `validation` and `openapi` only apply when a `backend` is present, and `fetch` only produces clients when there are backend routes to derive them from. [Details ›](/essentials/config#bringing-your-own-generator) #### Should I add my stack's Vite plugin to `viteConfig.plugins`? No - it reaches Vite through `stack`, so listing it in `viteConfig.plugins` runs the transform twice. To configure it, construct it yourself and pass it as `plugin`: `frontend: { stack: { name: "react", plugin: react({ jsxRuntime: "classic" }) }, base: "/" }`. With a bare name KosmoJS builds the plugin with the arguments the current command needs; an instance you pass is used as written, so set those yourself. [Details ›](/essentials/config#frontend-stack-required) #### How do I change the `/api` prefix, and where does it come from? It is `backend.base` in the folder's `kosmo.config.ts`, and it is a **full path** - not a suffix joined onto `frontend.base`. A route's URL is `backend.base` + route name, so `backend: { base: "/admin/api" }` serves `users/[id]` at `/admin/api/users/:id`. The `api/` **directory** name never appears in the URL - it only separates server routes from `pages/` on disk. [Details ›](/essentials/config#backend-base-required) #### Can I rename `VRefine`? Yes - set `refineTypeName` inside the `validation` options object. It stays globally available and import-free under whatever name you choose. [Details ›](/essentials/config#validation) #### How do I map an extra URL onto an existing route? The `alias` option in the `backend` block: `backend: { alias: { "/feed.xml": "rss" } }`. Keys are absolute URLs, not prefixed by the router's base; if a key has dynamic segments, their names must match the target route's parameters exactly or the request 404s. [Details ›](/backend/aliases) ### Directory-Based Routing #### How does routing map files to URLs? Folder names become path segments; `index` files define the endpoint or component: * `api/users/[id]/index.ts` maps to `/api/users/:id` * `pages/users/[id]/index.tsx` maps to `/users/:id`. No separate routing config - your file structure is your route definition. [Details ›](/routing/intro#how-it-works) #### Why directory-based instead of file-based? Clarity at scale: only `index.ts` is a route handler; every other file in the folder is an obviously-colocated helper. File-based routing leaves `schema.ts`/`auth.ts`/`utils.ts` ambiguous - route or helper? Directory-based removes that ambiguity. The only cost is creating a folder even when it holds just `index.ts`. [Details ›](/routing/rationale) #### Why must every route be a folder with an `index` file, even the root? Consistency - no special cases. The base route uses a folder named `index` (`pages/index/index.tsx` -> `/`). [Details ›](/routing/intro#how-it-works) #### How do nested routes work? Nest folders. `api/users/[id]/posts/index.ts` -> `/api/users/:id/posts`, as deep as your domain requires, each level colocating its own helpers, types, and tests without affecting siblings. Nesting also composes vertically: layouts wrap nested pages, and middleware cascades down the tree, so a parent segment's layout and middleware apply to everything beneath it. [Details ›](/routing/intro#nested-routes) #### Why the parallel `api/` and `pages/` structure? Intentional - a page and its corresponding API endpoint are always one folder apart and easy to find. [Details ›](/routing/intro#how-it-works) #### How do I create an API route? Create a folder under `api/` with an `index.ts` file - the folder path becomes the URL and KosmoJS seeds starter code automatically. For example, `api/products/index.ts` exposes `/api/products`, and `api/products/[id]/index.ts` exposes `/api/products/:id`. Inside, default-export a `defineRoute` that returns method handlers, then replace the seeded placeholder with real logic and visit the URL (e.g. `http://localhost:4556/api/products`). [Details ›](/routing/intro#route-file-requirements) #### How do I create a page? Create a matching folder under `pages/` with an `index` component file for your framework - `pages/products/index.tsx` (React/SolidJS), `.vue` (Vue), `.svelte` (Svelte), or `.mdx` (MDX) - and it becomes `/products`. KosmoJS seeds a placeholder component you replace with your own; the parallel `api/` and `pages/` trees mean a page and its endpoint are always one folder apart. The two sides are coupled by usage, not by name - you pick the names on each end freely. The docs mirror api and page names purely for consistency; matching them is a convention, not a requirement. Pages typically read data through the fetch client (`fetchClients["products"].GET()`). [Details ›](/routing/intro#route-file-requirements) #### What does the `_/` prefix and `_/api` map to? `_/` maps to `lib/` (derived code). `_/api` resolves to `lib//api.ts`, where `` is your source-folder name. [Details ›](/essentials/project-structure#path-mappings) #### What do `@/*`, `~/*`, `_/*` mean? Reserved path mappings: * `@/*` root-level imports * `~/*` source-folder imports, * `_/*` derived-code imports. Don't reuse these prefixes for your own aliases. [Details ›](/tutorial) ### Route Parameters #### What are the three parameter types? * `[id]` required (exactly one segment) * `{id}` optional (one segment or nothing) * `{...path}` splat (any number of segments). Same syntax for API routes and pages. [Details ›](/routing/params) #### How do I read a splat parameter? Matched segments come back as an array - for `/docs/guides/deployment/production`, `ctx.validated.params.path` is `["guides", "deployment", "production"]`. Useful for doc sites, file browsers, arbitrarily nested paths. [Details ›](/routing/params#splat-parameters) #### Why can't an optional param precede a required one? It creates ambiguity; `users/{optional}/[required]` is invalid. Optional params must not be followed by required ones (`users/{section}/{subsection}` is fine). [Details ›](/routing/params#optional-parameters) #### Why am I getting an unexpected 404 with an optional param before a static segment? With `properties/{city}/filters`, visiting `/properties/filters` makes the router match `{city}="filters"` and then expect another `/filters` segment that isn't there - 404. Fix it by adding an explicit static route (`properties/filters/index.tsx`), which takes priority. [Details ›](/routing/params#watch-out-for-ambiguous-paths) #### When does a static route win over a dynamic one? Always - static routes take priority over dynamic ones. [Details ›](/routing/params#watch-out-for-ambiguous-paths) #### How does a sibling `index` make `[id]` effectively optional? A parent `index` provides a fallback to render, so `careers/index.tsx` + `careers/[jobId]/index.tsx` makes `[jobId]` effectively optional. `{jobId}` communicates that intent more clearly; both notations work identically here. [Details ›](/routing/params#required-vs-optional-a-subtlety) #### How do mixed segments work? Static text + params in one segment: `[category].html`, `[id]-[data].json`, `[name].[ext]`. The folder is named with the mixed segment and `index.ts` lives inside it like any other route. [Details ›](/routing/params#mixed-segments) #### Which frontends support mixed segments? * *Backend* * Hono and H3: partial support with caveats. * Koa: full support. * *Frontend*: * Vue, Svelte, and MDX: full support. * React Router: `.ext` suffix only. * SolidJS: not supported. Prefer simple segments for frontend routes and keep mixed segments to the API side where support is complete. [Details ›](/routing/params#mixed-segments) #### What is power syntax? Raw `path-to-regexp v8` patterns passed through directly. The rule: any param name containing non-alphanumeric characters is treated as a raw pattern. Examples: `book{-:id}-info`, `locale{-:lang{-:country}}`, `api/{v:version}/users`. **Koa is the only backend with full support**: Hono matches but renames the params (`_0abc`), H3 won't match at all, and no frontend supports it - keep power syntax on the API side, on Koa. Read the path-to-regexp docs before using it in production. [Details ›](/routing/params#power-syntax) #### How do I make an optional static part (e.g. an optional `.html`)? e.g. `products/{:category.html}` - matches `/products` and `/products/electronics.html` but not `/products/electronics`. [Details ›](/routing/params#power-syntax) #### Does KosmoJS run its own path-to-regexp routing under the hood? No - path-to-regexp is used only at build time to parse your directory structure into route definitions. At runtime, those parsed routes are registered with each framework's native router exactly as you would register them by hand, so you keep the framework's full native routing: Hono's high-performance router on the backend, and React Router / Solid Router / Vue Router (with their nested layouts) on the frontend. KosmoJS is the chassis, not the engine - the engine is whichever framework you chose. [Details ›](/routing/intro#native-routing-under-the-hood) ### Seeded Boilerplate #### What happens when I create a route file? KosmoJS detects it and writes appropriate boilerplate - an API route (`defineRoute`) vs a page component, matched to your framework. You rarely write the skeleton by hand. [Details ›](/backend/custom-templates) #### Why doesn't my editor show seeded content immediately? Some editors load it instantly; others need a brief unfocus/refocus of the file. [Details ›](/backend/custom-templates) #### Why avoid anonymous arrow functions as default exports? A page's default export should be a named function (`export default function Page() {...}`) - an anonymous arrow can break Vite's HMR. [Details ›](/frontend/custom-templates) #### How do I override the default seeded template? Pass `templates` in the `frontend` or `backend` block of `kosmo.config.ts`, keyed by route-name glob pattern. Each value is either a template string or a function of the route returning one. Both blocks accept it: [Custom Page Templates ›](/frontend/custom-templates#configuration) · [Custom Route Templates ›](/backend/custom-templates#configuration) #### Which seeded files can templates override? Route `index` files and nothing else - `pages/**/index.*` on the frontend, `api/**/index.ts` on the backend. Every other seeded file gets the built-in minimal boilerplate to start from. [Frontend ›](/frontend/custom-templates) · [Backend ›](/backend/custom-templates) #### Why didn't my new template change an existing route file? Boilerplate is written **only into blank files** - work you have already done is never overwritten, so changing a template does not retroactively rewrite existing routes. Empty the file and it will be filled again. [Details ›](/backend/custom-templates) #### How does glob matching work for templates? `*` matches exactly one nesting level, `**` matches any depth, and an exact string targets a single route. Templates work with all parameter types (`users/[id]`, `products/{category}`, `docs/{...path}`, combined). [Details ›](/frontend/custom-templates#pattern-syntax) #### When multiple template patterns match, which wins? The first matching pattern, in the order the keys are written - so order them most-specific first (`landing/home` before `landing/*` before `**`). Caveat: JavaScript hoists integer-like keys to the front of an object, so a pattern such as `"2024/**"` matches before anything written above it; prefix it with `./` to keep your order. The same applies to `renderMode`, which uses the same resolver. [Details ›](/frontend/custom-templates#resolution-priority) #### How do templates help with CRUD seeding? Define one backend route template with the standard boilerplate - method handlers, validation targets, a declared `response` - and every seeded `api/**/index.ts` across many tables starts with the right structure instead of the skeleton being retyped N times. [Details ›](/backend/custom-templates#seeding-crud-endpoints) ### Backend #### Why does `defineRoute` repeat the route path I'm already in? Because TypeScript can't see the file system. Routing itself never uses the string - the URL comes from the file's location. The name is the key into the derived `RouteMap`, and that lookup is what types `ctx.validated.params` and the cascading `use.ts` context for that route. Since no runtime argument carries it, nothing can be inferred, so the type argument is required - and the seeded boilerplate already contains it. It cannot drift silently: `R extends keyof RouteMap`, so a stale name after a folder rename is a compile error. [Details ›](/backend/intro#the-route-name-type-argument) #### How do I define an endpoint? Default-export a `defineRoute` definition; the factory receives HTTP method builders and `use`, and returns an array of handlers. Import `defineRoute` from `_/api`. [Details ›](/backend/intro#defining-endpoints) #### Can I define multiple methods in one file? Yes - return `GET`, `POST`, `PUT`, `DELETE`, etc. in the array. [Details ›](/backend/intro#defining-endpoints) #### Does handler order matter? No - dispatch is by HTTP method. Undefined methods return `405 Method Not Allowed` automatically. [Details ›](/backend/intro#defining-endpoints) #### Which method builders exist? `HEAD`, `OPTIONS`, `GET`, `POST`, `PUT`, `PATCH`, `DELETE`. [Details ›](/backend/intro#defining-endpoints) #### What happens to a HEAD request if I only define GET? It is served by the `GET` handler, validated against its schemas, with the body dropped per the HTTP spec - `HEAD` is the one method that doesn't fall through to `405`. Define `HEAD` explicitly only when you want to override that - except on Hono, where you cannot: its router ignores any `HEAD` handler you define and fallback to `GET` handler. [Details ›](/backend/intro#defining-endpoints) #### Why method-based routing? In KosmoJS a single route folder owns one URL, and inside it you declare a handler per HTTP method - `GET`, `POST`, `PUT`, and so on - rather than branching on `ctx.method` or splitting verbs across files. This keeps everything about one resource in one place: the read, create, update, and delete logic for `/users/[id]` all live in that folder's `index.ts`, each as its own typed handler with its own validation and middleware. Because dispatch is by method, the handlers are declarative and order-independent - you list them in any order and the framework routes to the right one, returning `405 Method Not Allowed` automatically for verbs you didn't define. The style draws on Sinatra (2007), the Ruby framework that pioneered defining routes as `get "/path" do ... end` blocks - the same idea of a verb mapping straight to a handler, brought into a typed, directory-based structure. [Details ›](/backend/intro#defining-endpoints) #### Is there HMR for the API? Why does my in-memory state reset in dev? No HMR on backend. On the frontend, Vite patches modules in place; on the backend, changes trigger a hot reload: the whole program restarts, and module-level state resets with it. That's normal for a backend - it should be stateless so it can restart and scale. Keep persistent state in a real store (a database, even a local SQLite file), and close connections in `teardownHandler` so they don't leak across reloads. [Details ›](/dev-build-run/development-workflow#hot-reload-vs-hmr) ### Backend: Hono / H3 / Koa #### Hono/H3/Koa - which should I pick, and when does it matter? * *Hono*: Maximum performance, run unchanged across runtimes (Node, Deno, Bun, Workers), and prefer a clean, return‑based API. * *H3*: Similar to Hono in performance and multi‑runtime support, but with a stronger focus on Web standards and framework‑agnostic design. * *Koa*: Battle-tested, mature ecosystem, Node-focused. Great for traditional Node.js servers where you value stability and a large library ecosystem. [Details ›](/backend/intro) #### What's identical and what differs between Hono/H3/Koa implementation? Identical: route organization, middleware patterns, validation, the `use` API, slots, cascading middleware. Different: the context API inside handlers (body, params, state, error model). [Details ›](/backend/intro) #### How do params differ in Hono vs H3 vs Koa? Raw params: `ctx.req.param()` (Hono), `event.context.params` (H3), `ctx.params` (Koa) - all return untyped strings. Always prefer `*.validated.params` - the validated, refined type (e.g., number) with runtime validation automatically enforced. The validation logic is identical across all frameworks. [Details ›](/backend/context#route-parameters) #### How do I set the response in Hono/H3/Koa implementation? The native way for every framework: * *Hono*: `ctx.json(...)` / `ctx.text(...)` (return a Response‑like object) * *H3*: Return the value directly - an object is serialized as JSON (application/json), a string is sent as plain text (text/plain). You can also return a Response for full control. * *Koa*: `ctx.body = ...` (mutate the context) All three frameworks support setting status codes and headers as well (e.g., `ctx.res.status = 201` in Hono, `event.res.status = 201` in H3, `ctx.status = 201` in Koa). [Details ›](/backend/intro) #### How do the error models differ? * *Hono*: `app.onError()` catches everything (`await next()` does not throw). It captures any error thrown in handlers; returns a `Response`. * *H3*: Uses `app.use(onError(errorHandler))` as the global error handler. It captures any error thrown in handlers; returns a `Response`, plain object or string. * *Koa*: Errors bubble up through `await next()`. Koa emits an `error` event (for logging), but doesn't send a response automatically. Use `app.on("error", errorHandler)` to react on `error` event. Use `try`/`catch` around `await next()` in middleware to set `ctx.status`/`ctx.body`. [Details ›](/backend/error-handling) ### Backend: Context #### What is `ctx.bodyparser`? A unified parser API - `.json()`, `.form()`, `.raw()` - identical across frameworks. Results are cached, so calling the same parser repeatedly doesn't re-parse. [Details ›](/backend/context#unified-bodyparser) #### Do I usually call bodyparser directly? Rarely - defining a validation schema runs the appropriate parser automatically and places the result in `ctx.validated`. [Details ›](/backend/context#unified-bodyparser) #### What is `ctx.metaparser`? The same idea for request metadata - `.params()`, `.query()`, `.headers()`, `.cookies()`. Synchronous, and cached like the body parsers. `params()` and `query()` come back normalized - splats split into arrays, values coerced to your declared types - while `headers()` and `cookies()` are plain parses. [Details ›](/backend/context#unified-metaparser) #### What is `ctx.validated`? The validated, typed result for each target you defined: `ctx.validated.json`, `.query`, `.headers`, `.cookies`, `.form`, `.raw`, `.params`. [Details ›](/backend/context#validated-data-access) #### How do I access normalized data before validation runs? Through the parsers. The context is extended before any validator runs, so `ctx.metaparser` and `ctx.bodyparser` are already on it while `ctx.validated` is still empty. Useful in [edge middleware](/backend/edge-middleware) and in a [custom validator](/backend/middleware#overriding-validation). Both are cached, so reading there costs nothing: the validators and your handler reuse the same values, and the request stream is read once. [Details ›](/backend/context#unified-metaparser) #### Is normalized the same as validated? No. Normalizing splits splats and coerces types; it doesn't check anything. Your refinements run in the validators, and only the checked results land in `ctx.validated`. So `ctx.metaparser.query()` may hand you a number that your schema would still reject. [Details ›](/backend/context#unified-metaparser) #### Can I use the parsers in `api/app.ts`? No - `api/app.ts` runs before the context is extended, so there is no `ctx.metaparser`, no `ctx.bodyparser` and no `ctx.validated` yet. Read the request through the framework's own API, or move the check into `api/use.ts` under an `edge:` slot, which runs just after the context is extended. [Details ›](/backend/middleware#app-middleware) #### Do the raw params still work? Yes - `ctx.req.param()` (Hono), `event.context.params` (H3), `ctx.params` (Koa) still return raw strings if you need them. [Details ›](/backend/context#route-parameters) ### Backend: Middleware #### How do I add route-level middleware? Use the `use` builder inside `defineRoute`. By default middleware applies to all HTTP methods; call `next()` to continue, skip it to short-circuit. [Details ›](/backend/middleware#basic-usage) #### Where does global middleware live? `api/use.ts`, at the root of a source folder's `api/` directory. Whatever it default-exports runs for **every route in that folder** - no imports, no registration. It's seeded with the folder and is an ordinary file you edit; route templates never touch it. Use it for route-wide concerns (request id, a blanket auth check, permission checks, audit logging); anything narrower belongs in a subtree `use.ts` or the route's own `use`. [Details ›](/backend/middleware) #### Where does CORS go? `api/app.ts`, as [app middleware](/backend/middleware#app-middleware). Neither global nor edge middleware works for CORS - a preflight `OPTIONS` is answered before any route chain runs, so anything composed per route never sees it and the browser rejects the request. [Details ›](/backend/middleware#app-middleware) #### How do I return 401 instead of 400 when a token is bad? Give the auth middleware an `edge:` slot - `edge:auth`, say - and it runs ahead of validation, so an unauthenticated request is rejected before any schema is consulted. Declare it in `api/use.ts`, a cascading `use.ts` or the route itself; the file decides reach, the slot decides position. [Details ›](/backend/edge-middleware) #### So, where do I add my auth? Depends on whether any route needs to opt out. Same rules everywhere - put it directly in `api/app.ts`: one middleware, no slots, nothing downstream can replace it by accident, and it also covers unmatched URLs, preflights and `405`s. Some routes authenticating differently - a signature-verifying webhook, a public health check - use an `edge:` slot, so any route or subtree can substitute its own check. [Details ›](/backend/edge-middleware#so-where-do-i-add-my-auth) #### Can I have more than one edge middleware? As many as you like - give each its own name. `edge:auth`, `edge:ratelimit`, and so on: they run at the edge in declaration order, and each is its own slot, independently overridable. [Details ›](/backend/edge-middleware#name-your-slots) #### Do `edge:*` slots need a `UseSlots` declaration? No. Every `edge:` prefixed name is reserved, like the validation slots - nothing to add to `api/env.d.ts`. Typos are still caught, though: `edge-auth` is not a slot. [Details ›](/backend/edge-middleware#name-your-slots) #### What is the bare `edge` slot, and can I use it? It holds the built-in middleware that extends the context - the one that puts `ctx.metaparser` and `ctx.bodyparser` on it. Claim `edge` and you replace that, so nothing downstream has those helpers and validators, middleware and handlers all break. Always prefix your own: `edge:auth`, not `edge`. [Details ›](/backend/edge-middleware#name-your-slots) #### I put auth in `api/app.ts` and a route still declares `slot: "edge:auth"` - which one wins? Both run. An `edge:auth` slot substitutes an `edge:auth` entry declared above it; it can't replace anything in `api/app.ts`, because that layer isn't composed by KosmoJS at all. The route ends up authenticating twice, by two different rules - pick either one. [Details ›](/backend/edge-middleware#so-where-do-i-add-my-auth) #### Is `api/use.ts` the same as Express's `app.use()`? No - it runs **per route, not per request**. Global middleware is composed into each route's chain, so a request matching no route never reaches it, and it never sees requests outside this folder's `backend.base`. For work that must happen on every request regardless of routing, use the framework's own app instance in `api/app.ts`, where `appFactory`'s callback hands you `{ app }`. [Details ›](/backend/middleware) #### Can a route override or skip global middleware? Only if the global entry declares a `slot`. A route (or a cascading `use.ts`) declaring the same slot substitutes it - and the replacement runs **in the global one's position** in the chain, so surrounding order is preserved. A global middleware **without** a slot always runs and cannot be overridden or skipped, which is what you want for a security check. [Details ›](/backend/middleware#slot-composition) #### How does the onion model work? Middleware runs in definition order going in, then unwinds in reverse after the handler. Global `api/use.ts` runs first, then route-level `use`, then the handler, then back out. [Details ›](/backend/middleware#execution-order-onion-model) #### Why do `use` calls run before handlers regardless of array position? This is intentional, not a quirk of how you order the array. `use` registers middleware and the method builders (`GET`, `POST`, ...) register handlers; the framework always runs the middleware chain first, then the matched handler - so a `use` written after a handler in the array still runs before it. If you need logic to run *after* the handler, put it after `await next()` inside a middleware: code before `await next()` runs on the way in, code after it runs on the way back out (the onion model). [Details ›](/backend/middleware#execution-order-onion-model) #### How do I restrict middleware to specific methods? Pass the `on` option to `use`, listing the methods the middleware should run for. Handlers for other methods skip it: ```ts use(async (ctx, next) => { // ... return next(); }, { on: ["POST", "PUT", "DELETE"] }) ``` The same `on` option works in cascading `use.ts` files. [Details ›](/backend/middleware#method-specific-middleware) #### What are middleware slots? Named positions in the middleware chain - middleware with the same slot name replaces earlier middleware at that position, letting you override global defaults per-route without bypassing everything else. [Details ›](/backend/middleware#slot-composition) #### How do I register a custom slot name? Extend the `UseSlots` interface in `api/env.d.ts`, then use `{ slot: "yourName" }` anywhere. [Details ›](/backend/middleware#slot-composition) #### When I override via slot, does `on` inherit from what I'm replacing? No - `on` doesn't inherit; set it explicitly if needed. [Details ›](/backend/middleware#slot-composition) #### How do `use.ts` files wrap subtrees? Place `use.ts` in a folder and it automatically wraps all routes in that folder and its subfolders - no imports or wiring. `api/users/use.ts` wraps everything under `/api/users`; `api/users/account/use.ts` wraps only `/api/users/account`. [Details ›](/backend/cascading-middleware#how-it-works) #### What's the execution order across levels? Global `api/use.ts` -> parent folder `use.ts` -> current folder `use.ts` -> route handler. Parent always runs before child; children cannot skip parent middleware. [Details ›](/backend/cascading-middleware#how-it-works) #### What is `UseT`? A type every `use.ts` in an `api/` subfolder exports (even when empty) describing what the middleware adds to context. The global `api/use.ts` is the exception - it may export `UseT`, but the export is ignored there. These are merged so every route underneath is typed automatically - no imports, no type args on `defineRoute`. Inner definitions override outer ones, mirroring runtime. [Details ›](/backend/cascading-middleware#type-safe-context-extension) #### How do I extend `UseT` from a parent? Import the parent's `UseT`, intersect it, and re-export - avoiding duplicate definitions across the hierarchy. [Details ›](/backend/cascading-middleware#type-safe-context-extension) #### Why can some params be undefined in cascading middleware? A `use.ts` runs for every route in its subtree, including ones that don't define a given param - so the `id` param is present for `/users/[id]` but undefined for the `/users` route under the same `use.ts`. That's expected, not a bug. Keep cascading middleware generic - auth, logging, rate limiting; put param-specific logic in the route handler, where the param is guaranteed to exist. [Details ›](/backend/cascading-middleware#parameter-availability) #### How do I implement auth / logging / rate limiting? However your framework already does it - KosmoJS imposes nothing here and stays fully transparent. Any Hono/H3/Koa middleware package works unchanged, wired the native way for your framework. You can wire it directly in a route's `index.ts` via `use(...)`, or in an `api/` subfolder's `use.ts` to cascade it over a subtree. Nothing is KosmoJS-specific about the middleware itself; it's plain Hono/H3/Koa. [Details ›](/backend/cascading-middleware#common-use-cases) ### Backend: Error Handling #### Where is the default error handler? `api/errors.ts`, seeded per source folder - a regular file you can customize freely. [Details ›](/backend/error-handling#default-error-handler) #### How do I distinguish a ValidationError? `error instanceof ValidationError` (from `@kosmojs/core/errors`) -> respond 400 with field detail; otherwise use `error.statusCode || 500`. [Details ›](/backend/error-handling#default-error-handler) #### How do I do route-level error overrides? The default error handler lives in `api/errors.ts`, the same across all frameworks. The approach is uniform too: branch on the request path or context. [Details ›](/backend/error-handling) #### Why shouldn't I wrap handler logic in try-catch? Let errors propagate to the central error handler instead of swallowing them per-route. [Details ›](/backend/error-handling#let-handlers-fail) ### Validation #### What is runtype validation? TypeScript types are automatically converted to JSON Schema and validated at runtime - no separate schema language, no schemas drifting out of sync. One type definition is the source of truth for server validation, client (fetch) validation, and the OpenAPI spec. [Details ›](/validation/intro#understanding-runtype-validation) #### How does one type give both compile-time and runtime safety? The same definition that gives compile-time checking (autocomplete, refactor safety) also produces the runtime validator that runs when real requests arrive - closing the gap TypeScript can't cover at runtime. [Details ›](/validation/intro#understanding-runtype-validation) #### How are validators derived? AST parsing (via ts-morph / TFusion) extracts types and traces referenced files; AOT compilation produces high-performance validators in `lib` via TypeBox - direct property checks, not a generic JSON Schema interpreter. [Details ›](/validation/intro#how-derivation-works) #### Why is double (client + server) validation a performance gain, not a cost? Invalid requests are caught client-side before they leave the browser, saving bandwidth/compute and giving users instant feedback; server validation still runs for direct API calls. [Details ›](/validation/intro#end-to-end-validation) #### How do I refine params? Pass a tuple as the second type argument to `defineRoute`; each position maps to a param in path order (e.g. `<"users/[id]", [number]>`). A request to `/api/users/abc` is rejected with 400 before the handler runs. Refinements are positional, not name-based - renaming `[id]` to `[userId]` needs no change here. [Details ›](/validation/params#params-refinements) #### If URL params are strings, how does a `number` param validate? KosmoJS coerces the value before validation: type a param as `number` and `"123"` becomes `123`, while a non-numeric `"abc"` stays a string and fails the check with a clean 400. So you write `number` (or a numeric `VRefine`) and read a real number from `ctx.validated.params`, no manual coercing. [Details ›](/validation/params#params-refinements) #### Why must the params tuple be written inline? A pre-defined tuple *alias* loses the structural info needed to emit a schema. Individual type aliases used *inside* the inline tuple are fine - it's only extracting the whole tuple to a named type that breaks. [Details ›](/validation/params#params-refinements) #### What validation targets exist? Metadata (any method): `query`, `headers`, `cookies`. Body (POST/PUT/PATCH): `json`, `form`, `raw`. `form` covers both URL-encoded and multipart form data (so file uploads go here); `raw` accepts plain text, binary data, `Buffer`, `ArrayBuffer`, or `Blob`. [Details ›](/validation/payload#validation-targets) #### Can validation targets hold numbers? Only the `query` target will coerce numbers before validation: ```ts GET<{ query: { page: number } }>((ctx) => { const { page } = ctx.validated.query // page is a number }); ``` `headers`/`cookies`/`form`/`raw` never coerce numbers. ::: warning will never pass validation `POST<{ form: { age: number } }>` ::: `json` carries numbers natively, no coercion needed. [Details ›](/validation/payload#validation-targets) #### Can validation targets hold booleans? Only the `query` target will coerce booleans before validation - `"true"`/`"false"` become `true`/`false`: ```ts GET<{ query: { draft?: boolean } }>((ctx) => { const { draft } = ctx.validated.query // draft is a boolean (or undefined) }); ``` `params` (path segments, where a boolean is meaningless), `headers`/`cookies`/`form`/`raw` never coerce booleans. ::: warning will never pass validation `POST<{ form: { consented: boolean } }>` ::: `json` carries booleans natively, no coercion needed. For a non-`query` target, use a string union instead: ```ts POST<{ form: { consented: "true" | "false" | "on" | "off" } }> ``` #### Why one body target but multiple metadata targets? Body targets are mutually exclusive (one per handler - you can't have both `json` and `form`); metadata targets can be combined freely. A body target on GET, or two body targets, is flagged at dev time and the affected schema is disabled. [Details ›](/validation/payload#validation-targets) #### How do I handle file uploads? Use the `form` body target on a POST/PUT/PATCH handler - it accepts multipart form data, so the uploaded file and any accompanying text fields are validated together as one payload. Type the file field alongside the metadata fields (e.g. `form: { file: ..., title: string }`), and the parsed result is available on `ctx.validated.form` like any other validated body. [Details ›](/validation/payload#validation-targets) #### How do I validate responses, and why bother? The `response` property as a positional tuple: `[status, contentType, Schema]`, e.g. `[200, "json", User]`. It validates before sending (catching handlers that return incomplete objects or drifted DB/third-party shapes) and enables automatic OpenAPI derivation. [Details ›](/validation/response) #### Does response validation run in production? Not by default: in production, response validation is disabled by default. To enable, set `runtimeValidation: true` on the response target. There is no global switch: each handler enables its own response validation. [Details ›](/validation/response#development-vs-production) #### Can I use referenced types and generics? Fully supported - import shared types, use generic wrappers like `Payload`. Generics are resolved and every referenced type traced, and rebuilds the schema when a shared type changes. [Details ›](/validation/payload#referenced-types) #### Inline object type vs `Payload` for the `json` target - are they equivalent? Yes. An inline literal (`json: { email: VRefine }`) and a named wrapper (`json: Payload`) express the same thing - the validated body schema. Use inline for one-off shapes and a named type to reuse a domain model. [Details ›](/validation/payload#referenced-types) #### What is VRefine? It adds JSON Schema constraints to the target type - globally available, no import. `VRefine`. The first argument is the base type, the second is any valid JSON Schema validation keyword. [Details ›](/validation/refine) #### Which constraints apply where? * Strings: `minLength`, `maxLength`, `pattern`, `format` * Numbers: `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf` * Arrays: `minItems`, `maxItems`, `uniqueItems`. The second VRefine argument accepts any valid JSON Schema validation keyword, so the underlying keyword family is broader than the constraints listed here. [Details ›](/validation/refine) #### Why does `number` allow decimals, and how do I get integers? Plain `number` permits floats. Use `multipleOf: 1` for true integers - critical for DB IDs, where a float passes validation but gets rejected at the query level, turning a clear validation error into a confusing DB error. [Details ›](/validation/refine) #### How do I validate emails / date-times / patterns? `format: "email"`, `format: "date-time"`, or a `pattern` regex via VRefine. [Details ›](/validation/refine) #### What does a ValidationError expose? `target` (which request part failed: `params`/`query`/`headers`/`cookies`/`json`/`form`/`raw`/`response`), `errors` (array of `ValidationErrorEntry` with `keyword`/`path`/`message`/`params`/`code`), `errorMessage` (all errors as one string), `errorSummary` (e.g. "2 validation errors found across 2 fields"), `route`, and `data` (the data that failed). [Details ›](/validation/error-handling#validationerror-properties) #### How do I surface field-level form errors? Map `error.errors` to `{path, message}` pairs; `target` tells you which request part failed. Nested field paths use arrow notation (`customer -> address -> city`) - match them with word-boundary regex to avoid false positives. [Details ›](/validation/error-handling#validationerror-properties) #### How do I set custom per-field error messages? The second type argument to the handler accepts a per-target message set: an `error` fallback plus `"error.fieldName"` overrides (dot notation for nested fields, e.g. `"error.order.shipping.address.postalCode"`). KosmoJS picks the most specific message; it appears in each entry's `message`. [Details ›](/validation/error-handling#custom-error-messages) #### Why must I avoid built-in type names? Names like `Event`, `Response`, `Request`, `Error`, `Date`, `Partial`, `Record`, `Buffer` are referenced as-is during type flattening, so the validator sees the built-in, not your custom type - a silent runtime failure with no compile error. Use a consistent `T` suffix/prefix (`EventT`, `TResponse`). The full list is in the TFusion builtins reference. [Details ›](/validation/naming-conventions#why-this-matters) #### How do I skip runtime validation but keep types? Per-target `runtimeValidation: false` in the second type argument (works for payload and response). You then read the body via the bodyparser directly. Param validation cannot be skipped - params are part of the URL structure. For response targets the same flag is also the production opt-in: response validation only runs in production when set to `true`. Use sparingly: runtime validation is what catches mismatched DB responses, unexpected payloads, and API drift. [Details ›](/validation/skip-validation) #### Can I replace the default validator with my own? Yes - every target is middleware in a reserved slot, so claiming that slot runs your check instead: `validate:params`, `validate:query`, `validate:headers`, `validate:cookies`, `validate:json`, `validate:form`, `validate:raw`, `validate:response`. Declare it globally in `api/use.ts`, in a cascading `use.ts` for a subtree, or in the route itself for one endpoint. Reserved slots need no `UseSlots` declaration. [Details ›](/backend/middleware#overriding-validation) #### If I override one target, do the others still validate? Yes - slots are per target. Overriding `validate:json` replaces the JSON validator and nothing else; `ctx.validated.params`, `.query`, `.headers` and `.cookies` are still filled in by their own validators. [Details ›](/backend/middleware#what-you-take-over) #### What do I lose by overriding a validator? That target's entry in `ctx.validated`. The built-in validator loads the data, checks it and publishes the result; yours is only expected to check - so `ctx.validated.json` stays unset and the handler reads the body itself. Which costs nothing: `ctx.bodyparser.()` and `ctx.metaparser.()` are lazy loaded and cached, so your validator and your handler can both call them and the request stream is read once either way. [Details ›](/backend/middleware#what-you-take-over) #### `runtimeValidation: false` or a custom validator - which do I want? `runtimeValidation: false` turns checking off for that target and keeps the types. A slotted validator keeps checking, on your terms - the way in for a body format no schema describes, or a check that has to hit the database. [Details ›](/backend/middleware#overriding-validation) ### Type Safety #### What type arguments does defineRoute accept? * RouteName (required) * Params refinements tuple * Types unique to this specific route * Variables and Bindings for Hono * Context for H3 * State and Context for Koa [Details ›](/backend/type-safety#typing-state-context) #### How do I type Cloudflare bindings (e.g. D1) in Hono? The 4th type argument (`{ DB: D1Database }`) for a single route, or `DefaultBindings` in `api/env.d.ts` globally; read via `ctx.env.DB`. [Details ›](/backend/type-safety#typing-state-context) #### How do I add global context/state types? Declare them in `api/env.d.ts` via module augmentation: * `DefaultVariables`/`DefaultBindings` (Hono) * `DefaultContext` (H3) * `DefaultState`/`DefaultContext` (Koa) [Details ›](/backend/type-safety#global-context-types-api-env-d-ts) #### Can I relax TypeScript strictness, e.g. `exactOptionalPropertyTypes`? Yes, per source folder - the folder's `tsconfig.json` (`src//tsconfig.json`) extends the derived base config, so anything you set in its `compilerOptions` wins: ```json [src/front/tsconfig.json] { "extends": "../../lib/front/tsconfig.json", "compilerOptions": { // [!code ++:3] "exactOptionalPropertyTypes": false } } ``` `exactOptionalPropertyTypes: true` is the deliberate default: validated optional params round-trip exactly as declared (`status?: T` means *absent*, not `status: undefined`), which most codebases coming from looser configs notice immediately. Relaxing it - or any other strictness flag - is a per-folder choice and only affects that folder's typecheck. `compilerOptions` is the safe thing to add. An `include` is not: it replaces the base one rather than merging, and the base is what puts the folder, its `lib/` output and the ambient declarations in scope. [Details ›](/essentials/config#typescript-config) #### How do I typecheck? `pnpm typecheck` - the same shape as `dev` and `build`: no arguments checks every source folder, folder names check just those (`pnpm typecheck admin front`). It takes one extra argument the others do not: `.` for the project root. Each folder is checked against its own `tsconfig.json` - that's where JSX and framework settings live. Every selected tsconfig is checked even if an earlier one fails, so a single run reports all of them rather than stopping at the first. The command exits `1` if any of them reported errors, which makes it usable as a CI gate directly. A full run also checks the project root, so code outside `src/` can be covered. What it covers is up to you: the root `tsconfig.json` starts with only `./lib/*.d.ts` in its `include`, so the run happens but has nothing of yours in scope until you add paths to that list. The folders are checked separately rather than in one pass because each has its own path mappings - one `tsc` over everything would resolve `_/` against the wrong folder. [Details ›](/cli/typecheck) #### How do I typecheck only the project root? `pnpm typecheck .` meaning the project root rather than a source folder. Naming folders drops the root, so `pnpm typecheck admin` checks that folder only. `.` puts it back: on its own it checks the root and nothing else, and alongside folder names it adds the root to them - `pnpm typecheck . admin` checks both. Useful after touching a script or a shared package while `src/` is untouched. The root's `tsconfig.json` decides what "the root" covers, and out of the box that is nothing of yours - its `include` holds only `./lib/*.d.ts`. Add the paths you want checked, keeping that entry: `include` replaces rather than merges with the config it extends, so dropping it takes the ambient declarations in `lib/` out of scope. [Details ›](/cli/typecheck#selective-typechecking) ### Fetch Clients #### How are fetch clients derived? Automatically for every API route, derived from the same type definitions - change the API and the client updates, no manual sync. Output lands in `lib` alongside validators and the OpenAPI spec. [Details ›](/fetch/intro) #### How do I call a client? `import fetchClients from "_/fetch"`, then `fetchClients["users/[id]"].GET([123])`. [Details ›](/fetch/start#method-signatures) #### What's the method signature? First argument is a params array in path order; second is the optional payload (`{ query }`, `{ json }`, etc.). Every type is inferred from the route definition - params, payload, and response alike. [Details ›](/fetch/start#method-signatures) #### How do I call a route with no params or no payload? No params: call with no array (`fetchClients["users"].GET()`). Payload but no params: pass `[]` for params (`GET([], { query: {...} })`). If the route defines no payload, the second argument isn't required. [Details ›](/fetch/start#routes-without-parameters-or-payloads) #### Are requests validated before they leave the browser? Yes - clients validate params and payload before any network request, using the exact same TypeBox schemas as the server. Invalid data throws immediately, no round trip. [Details ›](/fetch/validation) #### What are validationSchemas, and how do I use them in forms? Each client exposes `validationSchemas` (`params`, `json.POST`, etc.) for real-time UI feedback. Four methods: `check(data)` (cheap boolean, safe per keystroke), `errors(data)` (field-level array, only after `check` fails), `errorMessage(data)` (one string), `errorSummary(data)` (brief overview). Gate the heavy three behind `check`. [Details ›](/fetch/validation#validation-schemas) #### How do I do performant per-field validation as users type? Schemas validate whole objects, so on a partially-filled form `check` fails for *all* missing required fields, not just the one under test. Fix: merge the field under test into a fully-valid placeholder payload (`{ ...validPayload, name: e.target.value }`) so `check` only fails for that field. On submit, always validate the real payload. Most forms don't need this - it matters for complex forms validating in real time. [Details ›](/fetch/validation#per-field-validation-performance) #### How do I build URLs without making a request? `path([123])` -> `/api/users/123`; `path([123], { query: { include: "posts" } })` adds a query string; `href("https://api.example.com", [123])` builds an absolute URL. Multiple params follow path order. [Details ›](/fetch/utilities) #### How do I distinguish ValidationError from network errors on the client? `import fetchClients, { ValidationError } from "_/fetch"`; `error instanceof ValidationError` means data failed validation and no request was made (it carries `target`/`errors`/`errorMessage`/`errorSummary`); anything else is a network or server error. [Details ›](/fetch/error-handling#catching-errors) #### How does it integrate with framework data patterns? Clients return standard promises, so they drop into SolidJS `createResource`, React Router `loader`/`useLoaderData`, TanStack Query `queryFn`, or a `useEffect` hook. Types flow through these abstractions automatically. Render-time patterns (loader, resource) also run during SSR in-process; a `useEffect` fetch only runs on the client. [Details ›](/fetch/integration) #### How do I get a route's response type on the client? You usually don't need to - awaiting a method already gives a typed result (`const user = await fetchClients["users"].GET([123])` types `user` from the route's response). For out-of-band typing - a `createAsync` accessor, a `useLoaderData()` result, a prop, a shared helper - import `ResponseT` from `_/fetch`, keyed by route name then method: `ResponseT["users"]["GET"]`. [Details ›](/fetch/type-safety#response-types) #### What does a fetch client return if the handler declares no `response`? `Promise`. Params and payload are always typed from the route definition, but the **return** value is opt-in: declare `response` on the handler and you get the typed result, response validation and the OpenAPI response schema together. `unknown` rather than `any` is deliberate - nothing was declared, so nothing is claimed. [Details ›](/fetch/type-safety#without-a-response-the-result-is-unknown) #### Why is my route missing from `ResponseT`? `ResponseT` is opt-in: an entry exists only for routes whose handler declares a `response` type. A route with no `response` has no `ResponseT` entry - the same reason it has no response validation. Add a `response` to the handler and the entry (and validation) appear together. [Details ›](/fetch/type-safety#response-types) #### What's the response type when a handler returns multiple responses? A handler can declare a union of responses; `ResponseT` collapses to a union of their body types, dropping any variant with no body (no third tuple element, like a bare `[409]`). So `[201, "json", User] | [202, "json", { queued: true }] | [409]` yields `User | { queued: true }`. [Details ›](/fetch/type-safety#multiple-responses) ### Frontend #### Which frontend frameworks are supported? React, SolidJS, Vue, Svelte, and MDX - directory routing bridges to each framework's native router and reactive model. [Details ›](/essentials/frameworks) #### How do I add a frontend or backend to an existing folder? Add the block to the folder's `kosmo.config.ts` - `frontend: { stack: "react", base: "/front" }` - then restart the dev server. The framework's Vite plugin is inserted for you - don't add the plugin yourself, or it runs twice. [Details ›](/essentials/config#the-shape) #### What `jsxImportSource` does each framework need? React `"react"`, SolidJS `"solid-js"`, Vue `"vue"` (only when using JSX), MDX `"preact"`. Mixing frameworks needs per-folder tsconfig - KosmoJS derives a `tsconfig.json` per folder in `lib/` for your folder's `tsconfig.json` to extend from. [Details ›](/frontend/intro#typescript-configuration) #### What foundation files does a source folder get? A root App component (your app shell), a router config (`routerFactory`), and a client entry point (`entry/client`). SSR adds a server entry. [Details ›](/frontend/intro#foundation-files) #### What is routerFactory? It wires your App + derived routes to the native router. Its callback returns `clientRouter()` (browser navigation) and `serverRouter(url)` (SSR routing). Derived routes are always wrapped inside your App, establishing the layout hierarchy, and use the folder's `baseurl`. [Details ›](/frontend/application#router-configuration) #### What is renderFactory? It orchestrates `mount()` (fresh client mount) vs `hydrate()` (hydrate SSR HTML), choosing automatically via `__KOSMO_HYDRATION_BOOL__` global var. Referenced from `index.html` through `entry/client`. [Details ›](/frontend/application#application-entry) #### Are page components lazy-loaded? Yes - all page components are lazy-loaded by default and fetched on demand, keeping the initial bundle small. The derived route shape differs slightly per framework's router format. [Details ›](/frontend/routing#lazy-loading) ### Layouts #### How do layout files work? A `layout` file in any `pages/` folder wraps every route in that folder and its subfolders; nest layouts by nesting folders. No imports or config - the file system defines the hierarchy, and child routes cannot escape parent layouts. [Details ›](/frontend/layouts#define-a-layout) #### What's the nested render order? Outermost App -> each layout in path order -> the page. E.g. for `/dashboard/settings/profile`: `App` -> `dashboard/layout` -> `dashboard/settings/layout` -> page. [Details ›](/frontend/layouts#define-a-layout) #### What's the recognized layout filename per framework, and is it case-sensitive? `layout.tsx` (React/SolidJS), `layout.vue` (Vue), `layout.svelte` (Svelte), `layout.mdx` (MDX) - lowercase only. Other casings are treated as regular components. Each folder runs one framework and ignores other frameworks' files (a Vue folder ignores `.tsx`, etc.). [Details ›](/frontend/layouts#layout-file-naming) #### Why isn't my pages/layout.\* file loaded? Layout files only apply inside route folders. A layout outside a route folder is not picked up. `pages/layout.*` files are simply ignored. If you look for a global layout that wraps every route, that's the `src//app.*` file. [Details ›](/frontend/layouts#global-layout-via-app-file) #### What does the root app file wrap, and how is it different from a layout? `src//app.*` at the source-folder root wraps every route - the place for truly global concerns (auth checks, analytics, error boundaries). A `layout` file only works inside a route folder, wrapping the whole subtree. Place it outside a route folder and it does nothing. [Details ›](/frontend/layouts#global-layout-via-app-file) #### How does each framework render the child route? React ``, Vue ``, SolidJS and MDX `props.children`, Svelte `{@render children()}`. [Details ›](/frontend/layouts#layout-implementation) #### How do I load data in a layout? Layouts are route-level, so a `layout.tsx` uses the same loader/preload a page does. React (`loader` + `useLoaderData`), Solid (`preload` + `query`/`createAsync`), Vue, Svelte, and MDX (all `loader` + `useLoaderData`) all load at the layout level, fetching shared data once for everything beneath it. For Vue, Svelte, and MDX a layout passes its path-qualified name to `useLoaderData` (e.g. `useLoaderData("dashboard/layout")`) to read its own data rather than the page's; React and Solid scope per route automatically. [Details ›](/frontend/layouts#data-loading-in-layouts) ### Navigation (typed Link) #### How does the typed Link component work? Just import it from `~/components/Link.{tsx,vue,svelte}`. The `to` prop takes a typed tuple `[routeName, ...params]` (e.g. `["users/[id]", 123]`), plus an optional `query` prop. Typing the route name triggers IntelliSense; parameterized routes require their params. [Details ›](/frontend/link-navigation#usage) #### What's the refactor-safety benefit? Renaming a route directory produces TypeScript errors at every `Link` referencing the old name - turning refactors into an automated checklist. [Details ›](/frontend/link-navigation#linkprops-type) ### Data Preload #### How does route-level preloading work per framework? `GET` here is a method off the fetch client for the route (`const { GET } = fetchClients["users/data"]`) - exported under the name the framework's router expects. * React: `export { GET as loader }` - React Router calls it before render; `useLoaderData()` retrieves the typed result with no duplicate request (runs on load, hover, navigation). * SolidJS: wrap the fetch in `query()` and export it as `preload` - called on hover/intent; `createAsync` calling the same `query`-wrapped function reuses the cached result. The raw client `GET` isn't cached, so the `query()` wrapper is what makes preload and `createAsync` share one fetch. * Vue: `export const loader` in a plain ` ``` ### Create a page - Svelte ```svelte

{user.name}

{user.email}

``` ### Create a page - MDX ```mdx // MDX: pages/users/[id]/index.mdx import fetchClients from "_/fetch"; import { useLoaderData } from "_/use"; const { GET } = fetchClients["users/[id]"]; export const loader = ({ params }) => GET([params.id]); export const UserPage = () => { const user = useLoaderData(); return

{user.name}

{user.email}

; }; ``` ### Create a page (cont.) 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 is derived 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 * [Project Structure](/essentials/project-structure) - what lives where, and what `@/` `~/` `_/` mean * [Configuration](/essentials/config) - every `kosmo.config.ts` option * [CLI](/cli/intro) - every command and flag, interactive and non-interactive * [Routing](/routing/intro) - parameters, mixed segments, power syntax * [Framework Support](/essentials/frameworks) - what each framework does and doesn't support * [Features](/features) - everything KosmoJS provides, at a glance --- --- url: /tutorial.md description: >- Build a full KosmoJS application step by step - routing, validation, middleware, fetch clients, pages, SSR, and multi-folder architecture. --- A step-by-step walkthrough covering everything KosmoJS provides. ## Create a Project ### Create a Project - npm ```sh npm create kosmo demo ``` ### Create a Project - pnpm ```sh pnpm create kosmo demo ``` ### Create a Project - yarn ```sh yarn create kosmo demo ``` ### Create a Project (cont.) A short interactive setup creates the project together with your first source folder, prompting for the framework and backend. The folder itself defaults to `app` at base `/` (override later, or via flags in CLI mode). It is also possible to bootstrap in the current folder, just use `.` as name: ```sh npm create kosmo . ``` Provide the framework/backend up front and no prompts appear: ### Create a Project - npm ```sh npm create kosmo demo -- --frontend solid --backend hono ``` ### Create a Project - pnpm ```sh pnpm create kosmo demo --frontend solid --backend hono ``` ### Create a Project - yarn ```sh yarn create kosmo demo --frontend solid --backend hono ``` ### Create a Project (cont.) The first source folder is always `app`, serving pages at `/` and its API at `/api`. Both `frontend` and `backend` can be further configured in `kosmo.config.ts`. Need no backend? Provide `--no-backend` flag: ### Create a Project - npm ```sh npm create kosmo demo -- --frontend solid --no-backend ``` ### Create a Project - pnpm ```sh pnpm create kosmo demo --frontend solid --no-backend ``` ### Create a Project - yarn ```sh yarn create kosmo demo --frontend solid --no-backend ``` ### Create a Project (cont.) Same for the frontend, provide the `--no-frontend` flag to get a backend-only setup. *** After bootstrap, `cd` into freshly created project (unless the project was bootstrapped in the current folder): ```sh cd ./demo ``` #### Install Dependencies ##### Install Dependencies - npm ```sh npm install ``` ##### Install Dependencies - pnpm ```sh pnpm install ``` ##### Install Dependencies - yarn ```sh yarn install ``` ##### Install Dependencies (cont.) What you have at this point is deliberately minimal: a `package.json` and your source folder's config with a few empty stub files. Follow next steps to turn this skeleton into a working app. ## Start the dev server The dev server completes the setup: it seeds the remaining project files and wires everything together. From then on it watches your routes and recomputes as you work: ### Start the dev server - npm ```sh npm run dev ``` ### Start the dev server - pnpm ```sh pnpm dev ``` ### Start the dev server - yarn ```sh yarn dev ``` ### Start the dev server (cont.) Your app is now running at `http://localhost:4556`. ## Create Your First API Route Create `api/users/[id]/index.ts` - KosmoJS detects the file and seeds boilerplate: ### Create Your First API Route - Hono ```ts // Hono: api/users/[id]/index.ts import { defineRoute } from "_/api"; export default defineRoute<"users/[id]">(({ GET }) => [ GET(async (ctx) => { return ctx.text("users/[id] route starts here - replace this response with real logic."); }), ]); ``` ### Create Your First API Route - H3 ```ts // H3: api/users/[id]/index.ts import { defineRoute } from "_/api"; export default defineRoute<"users/[id]">(({ GET }) => [ GET(async (event) => { return "users/[id] route starts here - replace this response with real logic."; }), ]); ``` ### Create Your First API Route - Koa ```ts // Koa: api/users/[id]/index.ts import { defineRoute } from "_/api"; export default defineRoute<"users/[id]">(({ GET }) => [ GET(async (ctx) => { ctx.body = "users/[id] route starts here - replace this response with real logic."; }), ]); ``` ### Create Your First API Route (cont.) > Some editors show seeded content immediately; others need a brief unfocus/refocus. Replace with real logic: ### Create Your First API Route - Hono ```ts // Hono: api/users/[id]/index.ts import { defineRoute } from "_/api"; export default defineRoute<"users/[id]">(({ GET }) => [ GET(async (ctx) => { const { id } = ctx.req.param(); const user = { id, name: "Jane Smith", email: "jane@example.com" }; return ctx.json(user); }), ]); ``` ### Create Your First API Route - H3 ```ts // H3: api/users/[id]/index.ts import { defineRoute } from "_/api"; export default defineRoute<"users/[id]">(({ GET }) => [ GET(async (event) => { const { id } = event.context.params; const user = { id, name: "Jane Smith", email: "jane@example.com" }; return user; }), ]); ``` ### Create Your First API Route - Koa ```ts // Koa: api/users/[id]/index.ts import { defineRoute } from "_/api"; export default defineRoute<"users/[id]">(({ GET }) => [ GET(async (ctx) => { const { id } = ctx.params; const user = { id, name: "Jane Smith", email: "jane@example.com" }; ctx.body = user; }), ]); ``` ### Create Your First API Route (cont.) With dev server running, visit `http://localhost:4556/api/users/123`: [Details ›](/backend/intro) ## Add Validation ### Parameter Validation Pass a tuple as the second type argument to refine params. Each position maps to a route parameter in order. Validation works identically across all frameworks, read validated params via `*.validated.params`: ```ts type User = { id: number; name: string; email: string } export default defineRoute<"users/[id]", [ number // [!code hl] ]>(({ GET }) => [ GET(async (ctx) => { const { id } = ctx.validated.params; // id is a validated number [!code hl] // ... }), ]); ``` Use `VRefine` for additional constraints (no import needed): ```ts defineRoute<"users/[id]", [ VRefine // positive integer ]> ``` Raw params still can be accessed via `ctx.req.param()`/`event.context.params`/`ctx.params`, but they are untyped strings - prefer `ctx.validated.params`. ### Payload/Response Validation The first type argument to each method handler defines validation targets. Metadata targets (any method): `query` · `headers` · `cookies` Body targets (mutually exclusive, POST/PUT/PATCH only): `json` · `form` · `raw` #### Payload/Response Validation - Hono ```ts // Hono: api/users/index.ts import type { CreateUserPayload, User } from "./types"; export default defineRoute<"users">(({ POST }) => [ POST<{ json: CreateUserPayload, // [!code hl] response: [200, "json", User] // [!code hl] }>(async (ctx) => { const { name, email, age } = ctx.validated.json; return ctx.json({ id: 1, name, email, age }); }), ]); ``` #### Payload/Response Validation - H3 ```ts // H3: api/users/index.ts import type { CreateUserPayload, User } from "./types"; export default defineRoute<"users">(({ POST }) => [ POST<{ json: CreateUserPayload, // [!code hl] response: [200, "json", User] // [!code hl] }>(async (ctx) => { const { name, email, age } = ctx.validated.json; return { id: 1, name, email, age }; }), ]); ``` #### Payload/Response Validation - Koa ```ts // Koa: api/users/index.ts import type { CreateUserPayload, User } from "./types"; export default defineRoute<"users">(({ POST }) => [ POST<{ json: CreateUserPayload, // [!code hl] response: [200, "json", User] // [!code hl] }>(async (ctx) => { const { name, email, age } = ctx.validated.json; ctx.body = { id: 1, name, email, age }; }), ]); ``` #### Payload/Response Validation (cont.) ```ts [./types.ts] export type CreateUserPayload = { name: string; email: VRefine; age?: number; } export type User = { id: number; name: string; email: string } ``` Payload is validated before your handler runs. Response is validated before it's sent. [Details ›](/validation/intro) ## Add Middleware For simple cases, wire middleware inline with `use`: ```ts import { logRequest } from "~/middleware/logging"; export default defineRoute<"users/[id]">(({ use, GET }) => [ use(logRequest), GET(async (ctx) => { /* ... */ }), ]); ``` For anything shared across routes, use cascading middleware instead. Create `api/users/use.ts` - it wraps every route under `/api/users` automatically: ```ts [api/users/use.ts] import { use } from "_/api"; export default [ use(async (ctx, next) => { // runs for every route under /api/users return next(); }) ]; ``` No imports in route files, no repetition. Parent `use.ts` files wrap child routes automatically. [Details ›](/backend/middleware) ## Fetch Clients Fetch clients are fully typed and validated client-side using the same high-performance TypeBox validators as the server - identical results, no duplication, no drift. Invalid requests are caught before they leave the browser: ### Fetch Clients - React ```tsx // React: pages/users/[id]/index.tsx import { useState, useEffect } from "react"; import { useParams } from "react-router"; import fetchClients from "_/fetch"; const { GET } = fetchClients["users/[id]"]; export default function UserPage() { const params = useParams(); const [user, setUser] = useState(null); useEffect(() => { GET([params.id]).then(setUser); }, [params.id]); // ... } ``` ### Fetch Clients - Solid ```tsx // Solid: pages/users/[id]/index.tsx import { useParams, 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])); // ... } ``` ### Fetch Clients - Vue ```vue ``` ### Fetch Clients - Svelte ```svelte ``` ### Fetch Clients - MDX ```mdx // MDX: pages/users/[id]/index.mdx import fetchClients from "_/fetch"; import { useLoaderData } from "_/use"; const { GET } = fetchClients["users/[id]"]; export const loader = ({ params }) => GET([params.id]); export const User = () => { const user = useLoaderData(); // ... }; ``` ### Fetch Clients (cont.) Server-side validation still runs even when endpoints are called directly - client validation is additive, not a substitute. [Details ›](/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 seeds framework-specific boilerplate. Add a layout for shared UI across route groups - create `pages/users/layout.tsx`: ```txt pages/ └── users/ ├── layout.tsx ← wraps all pages under /users └── index.tsx ``` Layouts can be nested - deeper layouts wrap inner layouts, matching your route hierarchy. [Details ›](/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 } from "@kosmojs/dev"; export default defineConfig({ frontend: { // ... ssr: true, // [!code ++] }, }); ``` > Restart dev server after changing `kosmo.config.ts`. KosmoJS seeds `entry/server.ts` - your SSR orchestration file. Critical CSS is extracted and inlined automatically; remaining styles load asynchronously. See it running - `pnpm preview` builds and serves the production output, and rebuilds whenever you save: ```sh pnpm preview ``` For deployment, `pnpm build` writes `dist/run.js`, which serves every source folder from one process. Folders are also bundled separately, so they can be deployed, scaled and run independently when that suits you better. [Details ›](/frontend/server-side-render) · [Dev / Build / Run ›](/dev-build-run/building-for-production) ## Add More Source Folders Your project starts with the source folder created at bootstrap. As the app grows, add more - one per distinct concern (main app, admin panel, marketing site, etc.). Each is independent with its own set of frameworks, config, base URL, etc. ### Add More Source Folders - npm ```sh npm run folder ``` ### Add More Source Folders - pnpm ```sh pnpm folder ``` ### Add More Source Folders - yarn ```sh yarn folder ``` ### Add More Source Folders (cont.) You'll be prompted for the frontend, backend, and more. The name gives the folder its prefixes: pages at `/`, API at `//api`. Non-interactive mode is also supported; pass any flag and no prompts appear: * `--frontend solid|react|vue|svelte|mdx` or `--no-frontend` (one required) * `--backend hono|h3|koa` or `--no-backend` (one required) * `--ssr` to enable server-side rendering * `--ssg` to enable static side generation * `--tsq` to enable TanStack Query ### Add More Source Folders - npm ```sh npm run folder -- --frontend solid --backend hono ``` ### Add More Source Folders - pnpm ```sh pnpm folder --frontend solid --backend hono ``` ### Add More Source Folders - yarn ```sh yarn folder --frontend solid --backend hono ``` ### Add More Source Folders (cont.) Need no backend? Provide the `--no-backend` flag for a frontend-only folder (a static docs or marketing site). Need no client? Provide the `--no-frontend` flag for a backend-only folder (an API service with no UI). The choice is always explicit - a forgotten flag is an error, never a silent default: ### Add More Source Folders - npm ```sh npm run folder -- api --backend hono --no-frontend # API only, no UI npm run folder -- docs --frontend mdx --no-backend # UI only, no backend ``` ### Add More Source Folders - pnpm ```sh pnpm folder api --backend hono --no-frontend # API only, no UI pnpm folder docs --frontend mdx --no-backend # UI only, no backend ``` ### Add More Source Folders - yarn ```sh yarn folder api --backend hono --no-frontend # API only, no UI yarn folder docs --frontend mdx --no-backend # UI only, no backend ``` ### Add More Source Folders (cont.) Creating a source folder adds framework-specific dependencies. Install them: ### Add More Source Folders - npm ```sh npm install ``` ### Add More Source Folders - pnpm ```sh pnpm install ``` ### Add More Source Folders - yarn ```sh yarn install ``` ## Directory-Based Routing Folder names become URL segments. Each route requires an `index` file: ```txt api/ users/ index.ts -> /api/users [id]/ index.ts -> /api/users/:id pages/ users/ index.tsx -> /users [id]/ index.tsx -> /users/:id ``` Parameters: `[id]` required · `{id}` optional · `{...path}` splat. Same pattern for API and pages - learn once, use everywhere. [Details ›](/routing/intro) ## Path Mappings Your project starts with a minimal `tsconfig.json`: ```json [tsconfig.json] { "extends": "./lib/tsconfig.json", "include": ["./lib/*.d.ts"] } ``` The extended config provides path mappings used throughout the framework. You can add your own paths, but these prefixes are reserved: * `@/*` - Root-level imports * `~/*` - Source folder imports * `_/*` - Derived code imports [Project Structure ›](/essentials/project-structure#path-mappings) walks the whole layout - what lives in `src/` versus `lib/`, and what each alias resolves to. *** ### 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](/dev-build-run/building-for-production)