---
title: 'Coming from SvelteKit'
slug: coming-from-sveltekit
description: 'A mapping of SvelteKit concepts to their Mochi equivalents for developers switching frameworks.'
---
## Coming from SvelteKit
You have likely already been using SvelteKit as your main framework for Svelte. Here is a quick list of most of the SvelteKit features and how they map to equivalent concepts in Mochi, so you can be up and running quickly.
### Routing
SvelteKit's file-based router (`src/routes/foo/+page.svelte`) is replaced by a programmatic routes record passed to `Mochi.serve({ routes })`. Each pattern is a Bun router string; each value is built with `Mochi.page`, `Mochi.api`, `Mochi.ws`, or `Mochi.sse`.
```
// SvelteKit
src/routes/+page.svelte → /
src/routes/posts/[slug]/+page.svelte → /posts/:slug
src/routes/health/+server.ts → /health
```
```ts
// file (Mochi): src/routes.ts
import { Mochi } from 'mochi-framework';
export const routes = {
'/': Mochi.page('./src/Home.svelte'),
'/posts/:slug': Mochi.page('./src/Post.svelte'),
'/health': Mochi.api(() => Response.json({ status: 'ok' })),
};
```
### Advanced routing
Mochi uses Bun's router patterns — `:slug` for required params, `*` for catch-all. There is no SvelteKit-style `[[optional]]` segment, no `[param=matcher]` syntax, and no `src/params/` directory. Validate the shape of a parameter inline.
```ts
// file (SvelteKit): src/params/fruit.ts
export function match(param: string): param is 'apple' | 'orange' {
return param === 'apple' || param === 'orange';
}
// then: src/routes/fruits/[name=fruit]/+page.svelte
```
```ts
// file (Mochi): src/routes.ts
import { Mochi, error } from 'mochi-framework';
export const routes = {
'/fruits/:name': Mochi.page('./src/Fruit.svelte', {
serverProps: ({ params }) => {
if (params.name !== 'apple' && params.name !== 'orange') error(404, 'Unknown fruit');
return { name: params.name };
},
}),
'/files/*': Mochi.page('./src/Files.svelte'),
};
```
Register the most specific patterns first — Bun matches in declaration order, not by file-name sort.
### Layouts
SvelteKit's `+layout.svelte` / `+layout.server.ts` have no Mochi equivalent. In SvelteKit, layouts persist across navigations — the client-side router keeps the layout component mounted and only swaps the page slot, which also lets `+layout.server.ts` skip refetching data that hasn't been invalidated. Mochi has no client-side router, so every navigation is a full page load; there is no component tree to persist and no data to selectively revalidate.
Instead, create a wrapper component that accepts `children`, and import it from each page.
```svelte
{@render children()}
```
```svelte
{@render children()}
```
In SvelteKit, `+layout.svelte` wraps `+page.svelte` automatically. In Mochi, the page must import and wrap itself:
```svelte
Posts
{#each posts as post}
{post.title}
{/each}
```
Share data that SvelteKit's `+layout.server.ts` would have loaded via a common helper, and spread the result into each route's `serverProps`:
```ts
// file (SvelteKit): src/routes/+layout.server.ts
export async function load() {
return { user: await loadCurrentUser() };
}
```
```ts
// file (Mochi): src/lib/baseProps.ts
export async function baseProps() {
return { user: await loadCurrentUser() };
}
```
```ts
// file (Mochi): src/routes.ts
import { Mochi } from 'mochi-framework';
import { baseProps } from './lib/baseProps';
export const routes = {
'/': Mochi.page('./src/Home.svelte', {
serverProps: async () => ({ ...(await baseProps()), posts: await loadPosts() }),
}),
};
```
Every page that shares a shell imports the layout component explicitly — there is no automatic nesting. This is more verbose than SvelteKit, but makes the component tree visible at each call site.
### Load functions
SvelteKit's `load` export becomes `serverProps` on your `Mochi.page` call. It's either a plain object or a `(req, params) => props` resolver (sync or async); the result is passed to the component as `$props`.
```ts
// file (SvelteKit): src/routes/posts/[slug]/+page.server.ts
export async function load({ params }) {
return { post: await loadPost(params.slug) };
}
```
```ts
// file (Mochi): src/routes.ts
'/posts/:slug': Mochi.page('./src/Post.svelte', {
serverProps: async (_req, params) => ({ post: await loadPost(params.slug) }),
}),
```
```svelte
{post.title}
```
The main component used with Mochi.page() renders on the server, so you can also call data helpers directly inside the component instead of threading every value through props. Client-side interactivity is opt-in per child component via `mochi:hydrate`, `mochi:hydrate:visible`, `mochi:defer`, or `mochi:defer:visible`.
You don't need to thread `params` through `serverProps`; instead, you can call `getRequestContext().params` directly anywhere on the server.
### Form actions
SvelteKit's `actions` export becomes the `actions` field on `Mochi.page`. The helpers are named the same: `fail`, `redirect`, `success`, imported from `mochi-framework`. POST submissions match an `?/` query (or `default` when absent), and the action's return value populates a `form` prop on re-render. The action callback receives `{ request, url, server, locals, kind, method, formData, actionName, cookies, params }`.
```ts
// file (SvelteKit): src/routes/login/+page.server.ts
import { fail, redirect } from '@sveltejs/kit';
export const actions = {
default: async ({ request }) => {
const formData = await request.formData();
const username = String(formData.get('username') ?? '');
if (!username) return fail(400, { error: 'Username required' });
return { username };
},
logout: () => redirect(303, '/'),
};
```
```ts
// file (Mochi): src/routes.ts
import { Mochi, fail, success, redirect } from 'mochi-framework';
export const routes = {
'/login': Mochi.page('./src/Login.svelte', {
actions: {
default: ({ formData, cookies }) => {
const username = String(formData.get('username') ?? '');
if (!username) return fail(400, { error: 'Username required' });
cookies.set('user', username, { httpOnly: true, path: '/' });
return success({ username });
},
logout: () => redirect(303, '/'),
},
}),
};
```
```svelte
{#if form?.error}
{form.error}
{/if}
```
When `actions` is declared, leave `form` reserved for the action result — don't return it from `serverProps`. See [Defining routes](/docs/defining-routes/).
### `use:enhance`
SvelteKit's `use:enhance` action becomes Mochi's `enhance` attachment from `mochi-framework`. The wire format and `submit` callback semantics match closely (`MochiEnhanceResult` mirrors SvelteKit's `ActionResult`).
```svelte
```
```svelte
```
Mark the surrounding component with `mochi:hydrate*` so the attachment can attach in the browser — attachments can only run when Svelte hydrates a component. See [Progressively enhancing forms with enhance](/docs/progressively-enhancing-forms-with-enhance/).
### API routes (`+server.ts`)
SvelteKit's `+server.ts` with `GET` / `POST` exports becomes `Mochi.api(handler)` — one handler per route, branch on `method` inside. The handler receives `{ request, url, server, locals, kind, method, params, cookies }`.
```ts
// file (SvelteKit): src/routes/api/users/[id]/+server.ts
export async function GET({ params }) {
return Response.json(await loadUser(params.id));
}
export async function POST({ params, request }) {
return Response.json(await createUser(params.id, await request.json()));
}
```
```ts
// file (Mochi): src/routes.ts
import { Mochi, error } from 'mochi-framework';
export const routes = {
'/api/users/:id': Mochi.api(async ({ method, request, params }) => {
if (method === 'GET') return Response.json(await loadUser(params.id));
if (method === 'POST') return Response.json(await createUser(params.id, await request.json()));
error(405, 'Method not allowed');
}),
};
```
Use `Mochi.page` for normal HTML routes — `Mochi.api` never goes through the error page or `handleError`. See [API routes](/docs/api-routes/).
### `error()`, `redirect()`, `fail()`
Same names, imported from `mochi-framework`. `error(status, message)` throws `MochiHttpError`; `redirect(status, location)` returns from an action; `fail(status, data)` and `success(data?)` round-trip via the `form` prop or the `enhance` envelope.
```ts
// SvelteKit
import { error, redirect, fail } from '@sveltejs/kit';
```
```ts
// Mochi
import { error, redirect, fail, success } from 'mochi-framework';
```
Call `error(status, message)` to signal an HTTP status — a bare `throw new Error()` becomes a 500.
### Error pages (`+error.svelte`)
SvelteKit's `+error.svelte` becomes the `errorPage` option on `Mochi.serve()` — a single component for the whole app (defaults to `DefaultError.svelte`). The component receives a single `error: MochiErrorProps` prop with `status`, `message`, and `stack` (dev only).
```svelte
```
See also [Error handling](/docs/error-handling/).
### Hooks (`handle`)
SvelteKit's `hooks.server.ts` `handle` export becomes the `handle` option on `Mochi.serve()`. The shape matches: `async ({ event, resolve }) => Response`, `event` carries `{ request, url, server, locals, kind }`, and `sequence(...handles)` composes them. Unlike SvelteKit, `event` itself does not carry `cookies`, `params`, or `getClientAddress` — read those from `getRequestContext()` inside the handler.
```ts
// file (SvelteKit): src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
event.locals.user = await loadUser(event.request);
return resolve(event);
};
```
```ts
// file (Mochi): src/handle.ts
import type { Handle } from 'mochi-framework';
export const auth: Handle = async ({ event, resolve }) => {
if (event.kind === 'asset') return resolve(event);
event.locals.user = await loadUser(event.request);
return resolve(event);
};
```
```ts
// file (Mochi): src/index.ts
import { Mochi, sequence } from 'mochi-framework';
import { auth } from './handle';
await Mochi.serve({ handle: sequence(auth), routes });
```
Wrap multiple handles in `sequence()` — `handle` takes a single function. See [Middleware (hooks)](/docs/middleware/).
### `resolve` options
SvelteKit's `resolve(event, { transformPageChunk, filterSerializedResponseHeaders })` maps to Mochi's `resolve(event, { transformPage, filterResponseHeaders })`. `transformPage({ html, done })` rewrites the HTML body; `filterResponseHeaders(name, value)` keeps or drops a header.
```ts
// file (SvelteKit): src/hooks.server.ts
export const handle = ({ event, resolve }) =>
resolve(event, {
transformPageChunk: ({ html }) => html.replace('%THEME%', 'dark'),
filterSerializedResponseHeaders: (name) => name.toLowerCase() !== 'server',
});
```
```ts
// file (Mochi): src/handle.ts
import type { Handle } from 'mochi-framework';
export const stripServer: Handle = ({ event, resolve }) =>
resolve(event, {
transformPage: ({ html }) => html.replace('%THEME%', 'dark'),
filterResponseHeaders: (name) => name.toLowerCase() !== 'server',
});
```
### `handleError`
Same name. Configure as a `Mochi.serve()` option; the hook receives `{ error, event, status, message }` and may return `{ status, message }`, a `Response`, or `void`.
```ts
// file (SvelteKit): src/hooks.server.ts
import type { HandleServerError } from '@sveltejs/kit';
export const handleError: HandleServerError = ({ error, event }) => {
tracker.capture(error, { path: event.url.pathname });
return { message: 'Internal error' };
};
```
```ts
// file (Mochi): src/index.ts
import type { HandleError } from 'mochi-framework';
const handleError: HandleError = ({ error, event }) => {
if (error) tracker.capture(error, { path: event.url.pathname });
};
await Mochi.serve({ handleError, routes });
```
`handleError` is never called for `Mochi.api` failures — return an error envelope inside the handler instead.
### `event.locals`
Same surface. Set `event.locals.x` from middleware; read it from any server-side code via `getRequestContext().locals`.
```ts
// file (SvelteKit): src/routes/profile/+page.server.ts
export const load = ({ locals }) => ({ user: locals.user });
```
```ts
// file (Mochi): src/SomePage.svelte
import { getRequestContext } from 'mochi-framework';
const { locals } = getRequestContext();
```
### Observability
SvelteKit's experimental OpenTelemetry tracing has no direct equivalent. Instead, Mochi emits structured lifecycle events through `mochiEvents` — `request`, `ws:*`, `sse:*`, `cache:*`, `action:*`, `server:start`, `server:stop`, and the compile-time events. Subscribe to hook tracing, metrics, or a custom logger in front of them.
```js
// file (SvelteKit): svelte.config.js
export default {
kit: { experimental: { tracing: { server: true }, instrumentation: { server: true } } },
};
// then write OTel setup in src/instrumentation.server.ts
```
```ts
// file (Mochi): src/index.ts
import { mochiEvents } from 'mochi-framework';
mochiEvents.on('request', ({ method, path, status, duration }) => {
metrics.timing('http.request', duration, { method, path, status });
});
```
`consoleLogger()` is the default subscriber for human-readable output. See [Events](/docs/events/).
### Client IP (`getClientAddress`)
SvelteKit's `event.getClientAddress()` becomes a method on `getRequestContext()`. Configure `proxy.addressHeader` / `proxy.xffDepth` on `Mochi.serve()` for trusted reverse-proxy hops.
```ts
// file (SvelteKit): src/routes/api/whoami/+server.ts
export const GET = ({ getClientAddress }) => new Response(getClientAddress());
```
```ts
// file (Mochi): src/handle.ts
import { getRequestContext } from 'mochi-framework';
const ip = getRequestContext().getClientAddress();
```
Set `proxy.xffDepth` so `getClientAddress()` picks the correct hop, instead of parsing `X-Forwarded-For` yourself.
### CSRF protection
Like SvelteKit, Mochi rejects cross-origin form POSTs by default (Origin header check on `POST`/`PUT`/`PATCH`/`DELETE` when the body is a form content type). Configure via the `csrf` option on `Mochi.serve()` and the `csrf:trustedOrigins`, `csrf:protectedMethods`, `csrf:formContentTypes`, `csrf:check` filters.
```js
// file (SvelteKit): svelte.config.js
export default {
kit: { csrf: { checkOrigin: true } }, // default
};
```
```ts
// file (Mochi): src/index.ts
await Mochi.serve({
csrf: { trustedOrigins: ['https://embed.example'] },
routes,
});
```
Pin trusted origins via `csrf.trustedOrigins` or the `csrf:trustedOrigins` filter instead of disabling CSRF on a state-mutating endpoint.
### Cookies
`event.cookies` becomes `getRequestContext().cookies` (also surfaced on the form-action callback as `event.cookies`). Same `get` / `set` / `delete` API and `CookieSerializeOptions`. App-wide defaults live on the `cookie:defaults` filter rather than per-call.
```ts
// file (SvelteKit): src/routes/login/+page.server.ts
export const actions = {
default: ({ cookies }) => {
cookies.set('session', token, { httpOnly: true, sameSite: 'lax', path: '/' });
},
};
```
```ts
// file (Mochi): src/handle.ts
import { getRequestContext } from 'mochi-framework';
const { cookies } = getRequestContext();
cookies.set('session', token, { httpOnly: true, sameSite: 'Lax', path: '/' });
```
### `$app/state` and the `page` store
There is no reactive `page` store. Instead, import `url`, `params`, `cookies`, and `locals` directly from `mochi-framework`. `url` is isomorphic — it reads from the request context on the server and from `window.location` on the client. `params` and `locals` are server-only.
```svelte
{page.url.pathname} — {page.params.slug}
```
```svelte
{url.pathname} — {params.slug}
```
`url` works on both server and client — no need to branch on environment. `params` and `locals` are server-only; guard them with `isServer` if needed. See [Request context](/docs/request-context/).
### `$app/navigation` (`goto`, `invalidate`, `preloadData`)
No equivalent. Mochi has no client-side router, so there is nothing to `goto` into and nothing to `invalidate` — every navigation is a full HTML round-trip. For form submissions, use `enhance()`; for anything else, set `window.location.href` or call `history.pushState` from a hydrated island.
```svelte
```
```svelte
```
Listen for `beforeunload` or `popstate` directly — there is no `beforeNavigate` / `afterNavigate` / `onNavigate`.
### Link options (`data-sveltekit-preload-*`)
No equivalent. There is no client router to preload code or data into — the browser handles `` clicks natively. `data-sveltekit-reload`, `data-sveltekit-replacestate`, `data-sveltekit-keepfocus`, and `data-sveltekit-noscroll` likewise have no Mochi attribute.
```html
About
```
```html
About
```
### Snapshots
No equivalent. SvelteKit's `snapshot` exists because its client router reuses page components across navigation; Mochi does full-page reloads, so the browser's bfcache restores native `` / `