---
title: 'Defining routes'
slug: defining-routes
description: 'Register pages, APIs, WebSockets, SSE endpoints, and file routes using the programmatic routes record.'
---
## Defining routes
Routes are a `Record` passed to `Mochi.serve({ routes })`. Each key is a Bun router pattern; each value is built 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
Patterns use Bun's router syntax: `:name` for a single segment, `*` for a wildcard tail. Read matched values from `getRequestContext().params`:
```ts
// file: src/index.ts
import { Mochi } from 'mochi-framework';
await Mochi.serve({
routes: {
'/posts/:slug': Mochi.page('./src/Post.svelte'),
},
});
```
```svelte
{params.slug}
```
Do **NOT** thread `params` through props from `serverProps`; instead, call `getRequestContext()` directly in the component or any helper it imports — the context is request-scoped via `AsyncLocalStorage`.
### `Mochi.page`
Register an SSR Svelte page via `Mochi.page(componentPath, { serverProps?, actions? })`. `componentPath` is resolved relative to the project root.
```ts
// file: src/index.ts
import { Mochi } from 'mochi-framework';
await Mochi.serve({
routes: {
'/about': Mochi.page('./src/About.svelte', {
serverProps: { title: 'About' },
}),
},
});
```
`serverProps` is either a plain object or a `(req, params) => props` resolver (sync or async). The resolved object is passed to 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),
}),
}),
},
});
```
```svelte
{post.title}
```
`actions` is a `MochiFormActions` map handling POST submissions to the route. See `Mochi.page actions` for the action contract.
Do **NOT** return a prop named `form` from `serverProps` when `actions` is declared; the name is reserved for the form action result and the route will throw at render time.
### `Mochi.api`
Register a JSON endpoint via `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` (via `error(status, message)`) for non-2xx responses; uncaught throws become `500 Internal Server Error`. See `API routes` for the full error contract.
Do **NOT** render HTML from `Mochi.api`; instead, use `Mochi.page` for HTML routes — API routes never go through the error page or `handleError`.
### `Mochi.ws`
Register a WebSocket endpoint via `Mochi.ws(handlers)`. `message` is required; `upgrade`, `open`, `close`, `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` for `upgrade` semantics and typed `ws.data.user`.
### `Mochi.sse`
Register a Server-Sent Events stream via `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));
}),
},
});
```
Do **NOT** forget `onClose` cleanup when you allocate per-connection resources; instead, register a teardown so timers/subscriptions don't leak when the client disconnects.
### `Mochi.file`
Serve a single file from disk via `Mochi.file(source)`. `source` is either a string path or a resolver `(req, params) => string` (sync or async) that returns the path. The `Content-Type` is inferred from the file extension; `HEAD` is handled automatically (headers only, empty body). Paths are resolved relative to the working directory; absolute paths work too, but every resolved path must stay inside the app root (the working directory) — anything outside returns a `404`.
```ts
// file: src/index.ts
import { Mochi, error } from 'mochi-framework';
await Mochi.serve({
routes: {
// Static path.
'/report': Mochi.file('./files/report.pdf'),
// Resolver — pick the file per request from the route param.
'/files/:name': Mochi.file((req, params) => {
if (!/^[a-z0-9-]+$/.test(params.name)) {
error(404, 'Not found');
}
return `./files/${params.name}.pdf`;
}),
},
});
```
A missing file returns a plain-text `404`; a resolver may also `error(404, …)` to force one. The file is read from disk on every request, so files written or deleted at runtime are picked up immediately. `Mochi.file` does **not** support `Range` requests, caching headers (`ETag`/`Cache-Control`), or middleware — reach for `Mochi.api` if you need full control over the response.
Route params are URL-decoded before they reach your resolver, so `params.name` can contain `../` (e.g. from `/files/..%2f..%2fsecret`). Mochi refuses to serve any path that resolves outside the app root, but that guard doesn't know which files _inside_ the root are private — `.env`, source files, and config are all fair game for a traversal that stays within the project. Always validate params against an allow-list or strict pattern, as above.
### HEAD requests
Every `Mochi.page` and `Mochi.api` route answers `HEAD` automatically by running its `GET`/handler logic and stripping the response body. Status and headers match the equivalent `GET`, and `Content-Length` is set to the byte length the `GET` body would have had. No per-route opt-in is needed — this also covers static assets and the `404` fallback.
`Mochi.sse` is GET-only: a `HEAD` is answered with `405 Method Not Allowed` (`Allow: GET`) without opening a stream, since a body-less probe of a stream endpoint can't reflect the real headers or run the same auth/observability path. `Mochi.ws` routes are upgrade-only and likewise do not handle `HEAD`.
### Static files
Files under `./public` are served automatically; no route entry is needed. A user-defined route always wins over a same-path public file. See `Serve options` for `publicDir`.