--- title: 'Defining routes' slug: defining-routes description: 'Register pages, APIs, WebSockets, SSE endpoints, and file routes with the programmatic routes record.' --- ## Defining routes Routes are a `Record` passed to `Mochi.serve({ routes })`. Each key is a Bun router pattern. Each value comes from one of the five `Mochi.*` helpers. ```ts // file: src/index.ts import { Mochi } from 'mochi-framework'; await Mochi.serve({ port: 3333, development: process.env.MODE === 'development', routes: { '/': Mochi.page('./src/Home.svelte'), '/about': Mochi.page('./src/About.svelte', { serverProps: { title: 'About' } }), '/health': Mochi.api(() => Response.json({ status: 'ok' })), '/ws/chat': Mochi.ws({ message(ws, msg) { ws.send(String(msg)); }, }), '/sse/time': Mochi.sse((stream) => { stream.send(new Date().toISOString()); }), }, }); ``` ### Route parameters Use `:name` for a single segment and `*` for a wildcard tail. Read matched values from `getRequestContext().params`. ```svelte

{params.slug}

``` A `:param` always captures the whole segment — you can't put literal text beside it. Bun reads `/profile/@:user` as literal text, so `/profile/@bob` never matches. Match `/profile/:user` instead; the sigil comes along in the param (`params.user === '@bob'`), and `/profile/bob` matches too. Guard and strip inline when you only want the prefixed form: ```ts serverProps: (_req, params) => { if (!params.user.startsWith('@')) error(404); return { username: params.user.slice(1) }; }; ``` ### `Mochi.page` Register an SSR Svelte page with `Mochi.page(componentPath, { serverProps?, actions? })`. `componentPath` resolves relative to the project root. `serverProps` is a plain object or a `(req, params) => props` resolver (sync or async). The resolved object reaches the component as `$props`. ```ts // file: src/index.ts import { Mochi } from 'mochi-framework'; await Mochi.serve({ routes: { '/posts/:slug': Mochi.page('./src/Post.svelte', { serverProps: async (_req, params) => ({ post: await loadPost(params.slug), }), }), }, }); ``` `actions` is a `MochiFormActions` map that handles POST submissions to the route. **Do not use `form` as a prop name.** When `actions` is declared, `form` is reserved for the action result. Return any other prop name from `serverProps` to avoid a runtime error. #### Redirecting from serverProps A `serverProps` resolver may return `redirect(status, location)` instead of props — the page render is skipped and the response carries the redirect. Use it for auth gates: ```ts // file: src/index.ts import { Mochi, redirect } from 'mochi-framework'; await Mochi.serve({ routes: { '/settings': Mochi.page('./src/Settings.svelte', { serverProps: (req) => { const user = currentUser(req); if (!user) return redirect(303, '/login'); return { user }; }, }), }, }); ``` The return type is `MochiRedirect`. Returning `fail()` or `success()` from `serverProps` is a runtime error — those are form-action results. ### `Mochi.api` Register a JSON endpoint with `Mochi.api(handler)`. The handler receives a `MochiApiEvent` (`method`, `request`, `url`, `server`, `locals`, `params`, `cookies`) and returns a `Response`. ```ts // file: src/index.ts import { Mochi } from 'mochi-framework'; await Mochi.serve({ routes: { '/health': Mochi.api(({ method }) => Response.json({ status: 'ok', method })), }, }); ``` Throw `MochiHttpError` with `error(status, message)` for non-2xx responses. An uncaught throw becomes `500 Internal Server Error`. See [API routes](/docs/api-routes/). ### `Mochi.ws` Register a WebSocket endpoint with `Mochi.ws(handlers)`. `message` is required. `upgrade`, `open`, `close`, and `drain` are optional. Return data from `upgrade` (or `false` to reject) to attach to `ws.data.user`. ```ts // file: src/index.ts import { Mochi } from 'mochi-framework'; await Mochi.serve({ routes: { '/ws/chat': Mochi.ws({ open(ws) { ws.subscribe('chat'); }, message(ws, msg) { ws.publish('chat', String(msg)); }, }), }, }); ``` See [WebSocket routes](/docs/websocket-routes/). ### `Mochi.sse` Register a Server-Sent Events stream with `Mochi.sse(handler)`. The handler receives a `MochiSseStream` with `send`, `close`, and `onClose`. ```ts // file: src/index.ts import { Mochi } from 'mochi-framework'; await Mochi.serve({ routes: { '/sse/time': Mochi.sse((stream) => { const interval = setInterval(() => stream.send(new Date().toISOString()), 1000); stream.onClose(() => clearInterval(interval)); }), }, }); ``` **Tear down anything you open per connection.** Each client opens its own timers, intervals, and subscriptions. Without a matching `onClose` teardown they keep running after the client disconnects and leak memory. ```ts Mochi.sse((stream) => { const unsubscribe = chat.subscribe((msg) => stream.send(msg)); stream.onClose(unsubscribe); // fires on disconnect or stream.close() }); ``` ### `Mochi.file` Serve one file from disk with `Mochi.file(source)`. `source` is a string path or a `(req, params) => string` resolver (sync or async). Mochi infers `Content-Type` from the file extension and answers `HEAD` automatically. Paths resolve relative to the working directory. Every resolved path must stay inside the app root. A path outside returns `404`. ```ts // file: src/index.ts import { Mochi, error } from 'mochi-framework'; await Mochi.serve({ routes: { '/report': Mochi.file('./files/report.pdf'), '/files/:name': Mochi.file((req, params) => { const name = params.name; if (!name || !/^[a-z0-9-]+$/.test(name)) { error(404, 'Not found'); } return `./files/${name}.pdf`; }), }, }); ``` Mochi reads the file from disk on every request, so files written or deleted at runtime are picked up at once. `Mochi.file` does not support `Range` requests, caching headers, or middleware. Use `Mochi.api` when you need full control over the response. Route params are URL-decoded before they reach your resolver, so `params.name` can contain `../` (for example, from `/files/..%2f..%2fsecret`). Mochi refuses any path that resolves outside the app root, but that guard does not protect private files inside the root, such as `.env`, source, and config. Always validate params against an allow-list or a strict pattern, as above. ### HEAD requests Every `Mochi.page` and `Mochi.api` route answers `HEAD` automatically. Mochi runs the `GET` logic and strips the body. Status and headers match the `GET`, and `Content-Length` is set to the `GET` body length. This also covers static assets and the `404` fallback. `Mochi.sse` is GET-only: a `HEAD` returns `405 Method Not Allowed` (`Allow: GET`) without opening a stream. `Mochi.ws` routes are upgrade-only and do not handle `HEAD`. ### Static files Mochi serves files under `./public` automatically, in development and production, so the directory must ship with your deploy. A user-defined route wins over a same-path public file. See [Serve options](/docs/serve-options/) for `publicDir`.