--- title: 'Welcome' slug: intro description: 'A lightweight, server-first Svelte 5 framework running on Bun that ships client-side JavaScript only for interactive islands.' --- # mochi Mochi is a lightweight, server-first framework for [Svelte 5](https://svelte.dev/) on [Bun](https://bun.sh/). Mochi websites render server-side on every request and ship as plain HTML. Components only ship JavaScript when you explicitly mark them as islands. ## Server-rendered, with island interactivity The websites we visit on the web are mostly static — text, images and links. Only a handful of elements on any given page actually need to be interactive: a search box, a logged-in badge, a comments widget. Mochi reflects this at the core of its design. Mochi sites renders server-side as plain HTML; the interactive pieces are marked with the `mochi:hydrate` directive and ship JS as interactive islands embedded in that HTML. Go ahead, try hydrating the page below and see which components will load JavaScript. The header text, the main column, and the footer ship as HTML and stay that way. The badge and the sidebar nav are wrapped as islands — same SSR HTML on first paint, with JS attached on top. Everything else is zero-JS forever. ## Why would I consider Mochi over SvelteKit? - **Faster sites.** Mochi ships zero client JavaScript by default. SvelteKit code-splits per route but still hydrates the entire page — even purely static content. Mochi only hydrates the components you explicitly mark as islands, which means less JS on first load, better bfcache behavior, and a natural fit for any site. - **Performant hydration.** Avoid hydrating islands until the users scrolls into them with `mochi:hydrate:visible`. Or avoid hydrating at all if the user never scrolls down to that component. Your users will thank you for the faster experience. - **Uses the platform.** Mochi ships with first-class support for View Transitions. No client side router, no state to keep track of between requests. - **No heavy bundler (no Vite).** Uses the lighting-fast Bun bundler, which builds sites with hundreds of routes in seconds. - **Real-time built in.** WebSockets and Server Sent Events are first-class route types — no extra packages or services required. **Mochi is in early development.** Only use in production if you are brave. ## Community Questions, bug reports, ideas, or just want to see what others are building? Join the [Mochi Discord](/discord/). --- title: 'Your first Mochi app' slug: your-first-mochi-app description: 'Build your first page with serverProps, selective hydration, and server islands in four steps.' --- ## Your first Mochi app Let's build a small app that exercises every server/client boundary you'll touch in real code. We'll put together a single `/hello` page in four steps, picking up one pillar at a time: [`serverProps`](/docs/defining-routes/) for loading data on every request, and [passing props to islands](/docs/island-props/) so a server-rendered parent can hand values to a hydrated child. Then we'll add [`mochi:hydrate`](/docs/selective-hydration/) for one interactive island while the rest stays zero-JS, and [`mochi:defer`](/docs/server-islands/) for a server island that renders separately from the main request. By the end we'll have a greeting card with a live like button and a personalized welcome that loads in after the page renders. ### Set up You'll need [Bun installed](https://bun.com/docs/installation) (>=1.3.13). Scaffold a new project with the official CLI and pick the **minimal** template when prompted: ```sh bun create mochi@latest my-app # choose: minimal cd my-app bun install bun run dev ``` The scaffold gives you a working app on `http://localhost:3333`. Its entry point is `src/index.ts`, which boots the server and declares your routes inline in the `Mochi.serve()` call: ```ts // file: src/index.ts (scaffolded) import { Mochi } from 'mochi-framework'; const PORT = Number(process.env.PORT) || 3333; await Mochi.serve({ port: PORT, development: process.env.MODE === 'development', routes: { '/': Mochi.page('./src/HelloWorld.svelte'), }, }); ``` `src/index.ts` is the single bootstrap file — you'll edit its `routes` object next, then build the Svelte components it points at. ### Step 1 — Register the route Now let's point `/hello` at a Svelte page and give it some data to render. Open `src/index.ts` and replace the scaffolded route inside `routes` with this one. `serverProps` is either a plain object or a `(req, params) => props` resolver — whatever it returns becomes the page component's `$props`. ```ts // file: src/index.ts import { Mochi } from 'mochi-framework'; await Mochi.serve({ port: Number(process.env.PORT) || 3333, development: process.env.MODE === 'development', routes: { '/hello': Mochi.page('./src/Hello.svelte', { serverProps: () => ({ siteName: 'Mochi', renderedAt: new Date().toISOString(), }), }), }, }); ``` The resolver runs on every request, so each reload produces a fresh `renderedAt`. See [Defining routes](/docs/defining-routes/) for the full `serverProps` contract and the other `Mochi.*` route helpers. (The scaffolded `src/HelloWorld.svelte` is now unused — feel free to delete it.) ### Step 2 — The page component Next, let's write the page itself. `Hello.svelte` stays server-only (all `Mochi.page()` entry components are server-only) — it consumes the `serverProps`, renders a static layout, and mounts the two child islands we'll build next. Notice that even though it imports two components that ship JavaScript, this file itself ships zero: the `mochi:` directives where we render the components decide what hydrates. ```svelte

Welcome to {siteName}

Rendered at {renderedAt}

Loading…

``` The `initialLikes={42}` value crosses the server→client boundary. Mochi serializes island props with [`devalue`](/docs/island-props/), so `Date`, `Map`, `Set`, `BigInt`, and cyclic references all survive the trip — not just JSON-safe values. Island props end up serialized into the HTML payload, so they're **visible to the client**. Never pass secrets, API keys, or session tokens this way. ### Step 3 — A hydrated island Now let's give the user something to click! `LikeButton.svelte` is a normal Svelte 5 component — we accept `initialLikes` as a prop, keep a `$state` counter, and bump it on click. ```svelte ``` The `mochi:hydrate` directive lives **where we render the component** in `Hello.svelte`, not inside the island itself. The same component can be mounted statically elsewhere. Reload the page in dev mode and you'll see Mochi's [debug bar](/docs/debug-bar/) pinned to the bottom-right of the page. Open the **Islands** panel — `LikeButton` shows up tagged `mochi:hydrate` with the byte size of its serialized props (the `initialLikes` value), and the crosshair icon next to each row scrolls to and outlines the island on the page. ### Step 4 — A server island Finally, let's add a personalized greeting that doesn't block the rest of the page. We marked `Visitor.svelte` with `mochi:defer` back in Step 2, so it skips the initial SSR pass — the page ships with our `

Loading…

` fallback in its place. The browser then fetches the component _in a separate request_, the server renders it, and the result swaps in. The deferred island fetch is its own request, so `getRequestContext()` inside the island sees the island URL — not the page URL. Read page-specific state in the parent and forward it as a prop. Update `Hello.svelte` to read `?name=` and pass it through to `Visitor`: ```svelte

Welcome to {siteName}

Rendered at {renderedAt}

Loading…

``` ```svelte

Welcome back, {name}!

``` `mochi:defer` lets the call site pass fallback children (our `

Loading…

`) that render in place of the island until the deferred fetch resolves. The framework handles the swap — `Visitor` itself never renders `children`, but we declare it in the prop type so TypeScript accepts the fallback at the call site. The `name` prop rides through the same `devalue` round-trip as `initialLikes`. Try [`/docs/your-first-mochi-app/hello?name=Alice`](/docs/your-first-mochi-app/hello?name=Alice) — the main page is identical for every visitor, but the deferred fragment swaps in a personalized greeting. Cookies are an exception worth knowing: the browser sends them along with the island fetch automatically, so `getRequestContext().cookies` inside a server island reads the visitor's cookies without needing the parent to forward them. ### See it live The finished app is running on this site at [**/docs/your-first-mochi-app/hello**](/docs/your-first-mochi-app/hello). Click the heart, then try [`/docs/your-first-mochi-app/hello?name=Alice`](/docs/your-first-mochi-app/hello?name=Alice) to watch the deferred fragment swap in a personalized greeting. The [debug bar](/docs/debug-bar/)'s **Islands** panel groups the two islands separately: `LikeButton` under hydrated islands as `mochi:hydrate`, and `Visitor` under server islands as `mochi:defer` with a lock icon (server-island props are HMAC-signed before being sent to the client). ### What's next - [Defining routes](/docs/defining-routes/) — `Mochi.page`, `Mochi.api`, `Mochi.ws`, `Mochi.sse`, and the full `serverProps` contract - [Selective hydration](/docs/selective-hydration/) — `mochi:hydrate`, `isHydratable`, `$props.id()` - [Lazy hydration](/docs/lazy-hydration/) — `mochi:hydrate:visible` for below-the-fold islands - [Server islands](/docs/server-islands/) — `mochi:defer`, signed props, and `MOCHI_KEY` - [Passing props to islands](/docs/island-props/) — every type `devalue` can round-trip --- 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/index.ts import { Mochi } from 'mochi-framework'; await Mochi.serve({ 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/index.ts import { Mochi, error } from 'mochi-framework'; await Mochi.serve({ 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/index.ts import { Mochi } from 'mochi-framework'; import { baseProps } from './lib/baseProps'; await Mochi.serve({ 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/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}

``` 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/index.ts import { Mochi, fail, success, redirect } from 'mochi-framework'; await Mochi.serve({ 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/index.ts import { Mochi, error } from 'mochi-framework'; await Mochi.serve({ 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

{page.status}

{page.error.message}

``` ```ts // file (Mochi): src/index.ts import { Mochi } from 'mochi-framework'; await Mochi.serve({ errorPage: './src/Error.svelte', routes: { '/': Mochi.page('./src/Home.svelte'), }, }); ``` ```svelte

{error.status}

{error.message}

``` 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 `` / ` ``` ```svelte ``` ### Shallow routing (`pushState` / `replaceState`) No framework helper. Call `history.pushState` / `history.replaceState` directly from a hydrated island; there is no `page.state` to read back, so store the value alongside the URL or in component state. ```svelte {#if page.state.modal}{/if} ``` ```svelte ``` ### Service workers No equivalent. There is no `src/service-worker.ts` convention and no `$service-worker` virtual module. Register one yourself from a hydrated island if you need offline support. ```ts // file (SvelteKit): src/service-worker.ts import { build, files, version } from '$service-worker'; const ASSETS = `cache-${version}`; // install / fetch handlers … ``` ```ts // file (Mochi): src/Boot.svelte (hydrated island) if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js'); } // then ship /public/sw.js yourself ``` ### Remote functions No equivalent. SvelteKit's type-safe `query` / `command` RPC has no built-in counterpart. The closest patterns are `Mochi.api(handler)` + hand-rolled `fetch()` for arbitrary calls, or `Mochi.page` actions + `enhance()` for form-driven mutations. ```ts // file (SvelteKit): src/routes/likes.remote.ts import * as v from 'valibot'; import { command } from '$app/server'; export const addLike = command(v.string(), async (id) => { await db.sql`UPDATE item SET likes = likes + 1 WHERE id = ${id}`; }); ``` ```ts // file (Mochi): src/index.ts import { Mochi, error } from 'mochi-framework'; await Mochi.serve({ routes: { '/api/like/:id': Mochi.api(async ({ method, params }) => { if (method !== 'POST') error(405, 'POST only'); await db.sql`UPDATE item SET likes = likes + 1 WHERE id = ${params.id}`; return Response.json({ ok: true }); }), }, }); // then on the client: await fetch(`/api/like/${id}`, { method: 'POST' }) ``` ### `$env/static/*` and `$env/dynamic/*` None of these virtual modules exist. Bun auto-loads `.env`; read everything via `process.env.FOO`. The `mochi` virtual module (`isServer`, `isBrowser`, `isDev`) covers the SSR-only / browser-only branching that `$env/static/private` solved by import-time errors. ```ts // SvelteKit import { API_KEY } from '$env/static/private'; import { PUBLIC_API_URL } from '$env/static/public'; ``` ```ts // Mochi const apiKey = process.env.API_KEY; const publicApiUrl = process.env.PUBLIC_API_URL; ``` Rely on Bun's built-in `.env` loading — no need for `dotenv`. If you need to beam an environment variable to the client, pass it as a prop to a hydrated island. ### `$app/paths` (`asset`, `base`, `resolve`) No equivalent. Mochi does not support sub-path deployments via configuration — write your app links as absolute paths (`/about`). The `assetPrefix` option on `Mochi.serve()` only rewrites the URL prefix for framework-internal bundles (default `/_mochi`), not for your own routes or static files. ```svelte About ``` ```svelte About ``` ### Server-only modules SvelteKit's `.server.ts` suffix carries over — Mochi uses the same convention. There is no `$lib/server` directory equivalent; the suffix is the entire mechanism, applied per-file anywhere in your source tree. Every named and default export of a `*.server.ts` file is replaced with a throwing `Proxy` on the client, so the real module body is only compiled for SSR. ```ts // file (SvelteKit): src/lib/server/db.ts import { Database } from 'better-sqlite3'; export const db = new Database(':memory:'); ``` ```ts // file (Mochi): src/lib/db.server.ts import { Database } from 'bun:sqlite'; export const db = new Database(':memory:'); ``` ```svelte

SQLite {version}

``` ### `$lib` No virtual `$lib` alias. Add the path to `tsconfig.json` if you want the same ergonomic. ```svelte ``` ```json // file (Mochi): tsconfig.json { "compilerOptions": { "paths": { "$lib/*": ["./src/lib/*"] } } } ``` ### Page options (`ssr`, `csr`, `prerender`) Not configurable per page. Mochi always renders on the server; client-side JavaScript is opt-in per component via `mochi:hydrate`, `mochi:hydrate:visible`, `mochi:defer`, or `mochi:defer:visible`. There is no prerender / SSG mode — every request renders fresh. Trailing-slash policy is global, not per-page: set `trailingSlash: 'never' | 'always'` on `Mochi.serve()` and Mochi registers both forms and redirects to the canonical one. See [Trailing slash](/docs/trailing-slash/). ```ts // file (SvelteKit): src/routes/about/+page.ts export const prerender = true; export const csr = false; export const trailingSlash = 'always'; ``` ```ts // file (Mochi): src/index.ts await Mochi.serve({ trailingSlash: 'always', routes }); // hydration is per-component: in the page ``` Control hydration with the `mochi:hydrate*` directives at each call site — there is no per-page `ssr` / `csr` / `prerender` export. ### Streaming / deferred data SvelteKit's pattern of returning promises from `load` to stream after the shell is replaced by `mochi:defer` server islands. The deferred component renders out-of-band against `${assetPrefix}/island/:componentName` (default `/_mochi/island/:componentName`) and swaps in once ready; its fallback children stay visible until then. ```ts // file (SvelteKit): src/routes/+page.server.ts export const load = () => ({ streamed: { recommendations: slowFetchRecommendations() }, }); ``` ```svelte {#await data.streamed.recommendations}

Loading…

{:then recs}
    {#each recs as r}
  • {r.title}
  • {/each}
{/await} ``` ```svelte

Loading…

``` See [Server islands](/docs/server-islands/). ### Adapters There is no adapter abstraction in Mochi. Instead, Mochi uses the bun standard `Bun.serve()` router - Bun is the only target. Containerise the Bun runtime or use a supported serverless platform to deploy. ### `vite.config.ts` Mochi does not use Vite. It compiles `.svelte` via Bun's own bundler using Svelte 5's compiler internally. Pass preprocessors through the `compile:preprocessors` filter; tweak the compiler via a `svelte.config.js` (Mochi auto-loads `./svelte.config.js`, override with the `svelteConfigPath` option). See [Svelte config](/docs/svelte-config/). ```ts // file (SvelteKit): vite.config.ts import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [sveltekit()] }); ``` ```js // file (Mochi): svelte.config.js export default { compilerOptions: { runes: true }, }; ``` ```ts // file (Mochi): src/index.ts await Mochi.serve({ filters: { 'compile:preprocessors': () => [vitePreprocess()] }, routes, }); ``` ### See also - [Defining routes](/docs/defining-routes/) — the four `Mochi.*` route helpers. - [Middleware (hooks)](/docs/middleware/) — `Handle`, `sequence`, `resolve` options. - [Error handling](/docs/error-handling/) — `errorPage`, `handleError`, API error envelope. - [Error boundaries](/docs/error-boundaries/) — auto-wrapped islands and ``. - [Server islands](/docs/server-islands/) — `mochi:defer` and deferred rendering. - [Hydratable values](/docs/hydratable/) — `hydratable(key, fn)` SSR→client value reuse. - [Extensions (hooks & filters)](/docs/extensions/) — `eventHooks` and `filters`. - [Events](/docs/events/) — `mochiEvents` lifecycle bus for observability and logging. - [Cache](/docs/cache/) — `MochiCache` SWR caching for arbitrary computations. - [Trailing slash](/docs/trailing-slash/) — global `trailingSlash` policy on `Mochi.serve()`. --- 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`. --- title: 'Selective hydration with mochi:hydrate' slug: selective-hydration description: 'Mark components with mochi:hydrate to ship client-side JavaScript only where interactivity is needed.' --- ## Selective hydration with `mochi:hydrate` Components render server-side by default and ship zero JavaScript. Add `mochi:hydrate` to opt a component into client-side hydration; everything else stays static HTML. ```svelte ``` Props are serialized with `devalue` into a ` {#if isHydratable} {:else} {count} {/if} ``` Do **NOT** declare `isHydratable` as a user-controlled prop; instead, treat it as a read-only input from the framework. ### Unique ids with `$props.id()` For a unique, SSR-stable id inside an island, use Svelte's native [`$props.id()`]() — the value generated during the server render is reused on hydration: ```svelte ``` Each component instance gets its own id, so repeating the same island on a page never produces duplicate DOM ids. It also works inside server islands: their standalone renders are namespaced with the island id carried inside the signed props envelope (via render's `idPrefix`), so ids from a deferred fragment cannot collide with ids already on the page. ### `mochi:hydrate:visible` Use `mochi:hydrate:visible` to defer hydration until the component scrolls into view. The component still server-renders; only its JS (and CSS) load on first intersection. ```svelte ``` Pass `rootMargin` to start loading before the component enters the viewport. See `Lazy hydration with mochi:hydrate:visible` for the full options. Islands that use `:visible` require JS to apply their styles — per-component CSS is loaded alongside the bundle on intersection, not in the initial page ``. If you need the island to look correcft on initial SSR load, do not use `:visible`. ### `mochi:defer` Use `mochi:defer` to render the component on a separate request after the page ships, and combine it with `mochi:hydrate` to also hydrate the deferred markup once it lands. See `Server islands with mochi:defer` for the full lifecycle. ```svelte ``` --- title: 'Lazy hydration with mochi:hydrate:visible' slug: lazy-hydration description: 'Defer island hydration until the component scrolls into view with mochi:hydrate:visible.' --- ## Lazy hydration with `mochi:hydrate:visible` Defer hydration until a component scrolls into the viewport. The component still renders server-side on every request, but its JavaScript and CSS are fetched only when the wrapper intersects the viewport via `IntersectionObserver`. ```svelte ``` Pass an options object to start loading before the element enters the viewport. `rootMargin` is forwarded straight to `IntersectionObserver`: ```svelte ``` The default `rootMargin` is `'0px'` — hydration fires the moment the island's first child crosses the viewport edge. Once intersection fires the observer disconnects, the component bundle imports, the deferred CSS link is appended to ``, and Svelte hydrates the existing SSR markup. Do **NOT** assume the island is fully styled before hydration; instead, accept that lazy islands flash unstyled until their CSS link loads. Bundle critical above-the-fold styles into the page shell or use `mochi:hydrate` for anything that must look right pre-hydration. Do **NOT** nest `mochi:hydrate:visible` inside another hydratable island; instead, hoist it to the page level — nested hydration directives are rejected at compile time. ### Combining with `mochi:defer` Stack `mochi:defer mochi:hydrate:visible` to defer both rendering and hydration: the placeholder ships with the page, the SSR HTML streams in when the deferred fetch resolves, and the JavaScript loads only after the now-rendered island scrolls into view. ```svelte ``` See `Selective hydration` for the eager `mochi:hydrate` directive and `Server islands` for `mochi:defer` on its own. --- title: 'Server islands with mochi:defer' slug: server-islands description: 'Render components after initial page load by fetching their HTML from the server with mochi:defer.' --- ## Server islands with `mochi:defer` Mark a component with `mochi:defer` to skip it during the initial SSR pass and render it on-demand from a dedicated endpoint after the page loads. Use this for personalized fragments (avatars, cart counts) that would otherwise block the surrounding HTML from being cached. ```svelte ``` Children of a deferred component become the fallback shown until the island resolves: ```svelte
Loading...
``` Server island components are normal Svelte components with full access to the request context via `getRequestContext()` — cookies are forwarded automatically because the fetch is same-origin. ```svelte

Welcome back, {userName}!

``` ### Fetch flow 1. SSR emits a `` custom element holding the fallback content; the component itself is **not** rendered. 2. Props are serialized with `devalue`, HMAC-signed, and stamped onto the element as `signed-props`. 3. On `connectedCallback`, the element fetches `/_mochi/island/{ComponentName}?props={signedProps}` (the `/_mochi` prefix follows `assetPrefix`). 4. The server verifies the signature, decodes the props, renders the component, and returns the HTML. 5. The HTML replaces the fallback inside the custom element. Failed fetches are retried with exponential backoff (default 5 retries, 1s–10s); pass `mochi:defer={{ retries: 10 }}` to override. ### Combining with hydration Apply `mochi:hydrate` alongside `mochi:defer` to fetch the island on-demand and then hydrate it for client-side interactivity: ```svelte ``` ### Lazy server islands with `mochi:defer:visible` Defer the _fetch_ until the wrapper scrolls into view, mirroring [`mochi:hydrate:visible`](lazy-hydration/): ```svelte
Loading...
``` Pass `rootMargin` to start fetching before the island enters the viewport. `rootMargin` and `retries` can be combined: `mochi:defer:visible={{ rootMargin: '200px', retries: 10 }}`. Combinable with `mochi:hydrate` / `mochi:hydrate:visible` for interactive lazy islands. Provide fallback children when using `:visible` so the user has something to scroll past while waiting. ### Props Props are serialized with `devalue` — see [Passing props to islands](island-props/) for the full list of supported types. Server islands additionally HMAC-sign the payload and pass it as a query parameter; if the signed props exceed URL length limits (~1800 bytes), a warning is emitted. Do **NOT** ship large blobs through server-island props; instead, fetch the data inside the component using `getRequestContext()`. Signed-prop URLs over 1800 chars trigger a runtime warning. ### Signing key Props are signed with a 32-byte key resolved at startup from `process.env.MOCHI_KEY` (base64url-encoded). If `MOCHI_KEY` is unset, Mochi generates a random key and logs a warning — fine for local dev, broken across restarts and multi-instance deploys. ```sh # .env MOCHI_KEY= ``` Generate one and write it to `.env` with [`mochi-framework generate-key`](/docs/cli#generate-key): ```sh bunx mochi-framework generate-key ``` **Set `MOCHI_KEY` for any deployment that runs more than one process or survives restarts.** Without a shared key, signatures minted by one instance won't verify on another and deferred islands will fail to load after a restart or rolling deploy. Do **NOT** commit `MOCHI_KEY` to version control; instead, supply it through your platform's secret store. Generate one with `openssl rand -base64 32 | tr '+/' '-_' | tr -d '='`. --- title: 'Passing props to islands' slug: island-props description: 'How props are serialized and passed to hydratable islands, including supported types and auto-injected framework props.' --- ## Passing props to islands Pass props to a component marked with `mochi:hydrate`, `mochi:hydrate:visible`, or `mochi:defer` exactly as you would to any Svelte component — the framework serializes them with [`devalue`](https://github.com/Rich-Harris/devalue) so the same values reach the hydrating client. ```svelte ``` Do **NOT** pass functions, class instances, or `Symbol` values as props; instead, send a plain-data representation and rebuild the value inside the island. ### Typing props Put the type on the `let { … } = $props()` declaration — don't pass a type argument to `$props()` itself. For a handful of props, inline the type after the destructuring: ```svelte ``` For larger or reused shapes, pull it out into a `Props` interface: ```svelte ``` Avoid the `$props<{ … }>()` type-argument form — always annotate the `let { … }` declaration as shown above. Snippet props (including `children`) are typed with the `Snippet` interface from `svelte`: ```svelte {@render children()} ``` When a component wraps a native element and forwards the rest of its attributes, type the spread with the matching interface from [`svelte/elements`](https://svelte.dev/docs/svelte/typescript#Typing-wrapper-components): ```svelte ``` ### Wire format For `mochi:hydrate*` islands, props are emitted as a ` ``` - `isHydratable` — `true` when the call site uses `mochi:hydrate`, `mochi:hydrate:visible`, or `mochi:defer mochi:hydrate`. Undefined for pure SSR-only invocations and for bare `mochi:defer`. Do **NOT** declare `isHydratable` as a user-controlled prop; instead, treat it as an input the framework owns. See `Selective hydration with mochi:hydrate` for the branching pattern. For a unique per-instance id, use Svelte's native `$props.id()` instead of a prop. `islandId` is a reserved name on every island (`mochi:hydrate` and `mochi:defer` alike) — passing it as a literal prop is a compile error, so a component can move between directives without the name silently changing meaning. On `mochi:defer` it is also the framework's transport key inside the signed envelope, stripped server-side before the component renders; a spread carrying it there is overridden by the framework value (last key wins). For a unique id inside the component, use `$props.id()`. --- title: 'Hydratable values' slug: hydratable description: 'Serialize computed server values into the page so the client can reuse them without re-running the work.' --- ## Hydratable values (experimental) > hydratable support is experimental, please create an issue if you find problems! 🙇 Svelte 5's [`hydratable(key, fn)`](https://svelte.dev/docs/svelte/svelte#hydratable) computes a value on the server, serializes it into ``, and reads it back during client hydration instead of recomputing. Use it to avoid running the same async work twice when a hydrated component does data fetching at the top level. Without it, the function runs once on the server and again during hydration: ```svelte

{user.name}

``` With it, the server result is reused on the client: ```svelte

{user.name}

``` Mochi already wires this up: any `hydratable()` call inside a `Mochi.page(...)` route or a `mochi:hydrate*` island is collected into the page's head script during SSR and picked up by Svelte's `hydrate()` automatically. There's no extra Mochi-side import — `hydratable` comes straight from `svelte`. See it in action in the [Hydratable demo](/demos/hydratable/), where the page and a hydrated island share the same key — the server function runs once, both sides render the same value, and the island skips the async work on hydration. **Namespace your keys.** Hydratable keys are global per-render. Prefix every key with your app or library name (`app:user`, `mylib:cart`) so two unrelated callers can't collide. ### Serialization Values are serialized with [`devalue`](https://www.npmjs.com/package/devalue), so `Map`, `Set`, `Date`, `URL`, `BigInt`, and circular references all round-trip. Promises also work — Svelte stitches them back together on the client. ### Limitations in Mochi today **Server islands.** `mochi:defer` server islands render in a separate request, and their `` output is not merged into the parent page. `hydratable()` calls inside a server island won't reach `window.__svelte.h` — keep them in the page or in eagerly hydrated islands for now. **No CSP nonce wiring.** Mochi does not yet pass a `csp.nonce` through to Svelte's `render()`, so the inline lookup script will be blocked under strict `script-src` policies. Either allow `'unsafe-inline'` for scripts (which you likely already do for the island bootstrap) or wait for nonce plumbing. --- title: 'Server-only imports' slug: server-only-imports description: 'Keep server-only modules like bun:sqlite out of client bundles using the .server.ts convention.' --- ## Server-only imports Any module reachable from a hydratable island gets bundled into the client. To use a server-only library (`bun:sqlite`, `node:fs`, anything that touches the filesystem) from inside an island, put the library plus a thin wrapper in a `*.server.ts` file. Mochi replaces these files with throwing-Proxy stubs on the client; the real module is only compiled for SSR. ```ts // db.server.ts import { Database } from 'bun:sqlite'; const db = new Database(':memory:'); export const getVersion = (): string => (db.query('SELECT sqlite_version() as v').get() as { v: string }).v; ``` ```svelte

SQLite {version}

``` The `.server.ts` (or `.server.js`) suffix is the entire convention — no runtime API to call, no config. Import with the extension (`./db.server.ts`); extensionless `./db.server` also works. Types follow the import normally. **Wrap usage in `hydratable()` or `isServer`.** The stub throws on access. If you call a `.server.ts` export from client-running code (an `onclick` handler, an `$effect`), the page will throw at runtime. `hydratable()`'s producer function never runs on the client — it reads the value cached at SSR time — so wrapping the call there is safe. Read the value once on the server, ship it through `hydratable()` or a prop, and use the resolved value on the client. ### What gets stubbed Every named and default export of a `.server.ts` file is replaced with a `Proxy` that throws on both function calls and property access. The error message names the export and its origin file so a stray client-side use surfaces cleanly: ``` getVersion from /…/db.server.ts was called on the client; this is a server-only export. ``` ### Not supported - `export * from './x'` — Mochi warns at build time; declare named exports in the `.server.ts` file directly. - `.server.svelte` — component-level convention is not provided. Put server-only code in plain TS and call it from a component. --- title: 'Progressively enhancing forms with enhance' slug: progressively-enhancing-forms-with-enhance description: 'Progressively enhance HTML forms to submit via fetch when JavaScript is available.' --- ## Progressively enhancing forms with enhance `enhance` is a Svelte attachment that progressively enhances `
`. The same server action runs whether JS is available or not; with `{@attach enhance(...)}` the client submits over `fetch`, the server returns a JSON `MochiEnhanceResult` envelope, and there is no full-page reload. ```svelte
``` Place the form inside a hydrated island (`mochi:hydrate`, `mochi:hydrate:visible`, or `mochi:defer mochi:hydrate`). When hydration is skipped the attachment never runs and the form falls back to a native HTML POST — that is the progressive-enhancement contract. Do **NOT** call `enhance()` outside a hydrated island; instead, mark the surrounding component with `mochi:hydrate*` so the attachment can attach in the browser. `enhance` is a factory: call it as `{@attach enhance()}` even with no options. Attachments require Svelte 5.29+. ### Wire format `enhance` adds `Accept: application/json` and `x-mochi-action: true` to the POST. The server detects either header and responds with one of four shapes: ```ts type MochiEnhanceResult = | { type: 'success'; status: number; data?: unknown } | { type: 'failure'; status: number; data?: unknown } | { type: 'redirect'; status: number; location: string } | { type: 'error'; status?: number; error: unknown }; ``` HTTP status is `200` for `success`, `failure`, and `redirect`; the body's `status` field carries the action's status. For `error`, the HTTP status matches the error code. `data` is encoded with [devalue](https://www.npmjs.com/package/devalue) so `Date`, `Map`, `Set`, `BigInt`, and cyclic references survive the wire. ### Default fallback Without a callback, `enhance` runs a minimal default per result type: | `result.type` | Default | | ------------- | ------------------------------------------------- | | `success` | `form.reset()` | | `failure` | nothing (provide a callback to update the UI) | | `redirect` | `window.location.assign(result.location)` | | `error` | `console.error('[mochi] enhance:', result.error)` | **The default fallback is intentionally lean.** Mochi has no client-side `page.form` store, no `goto`, and no `invalidateAll` — the framework cannot auto-update component props or re-run server data after a submission. Pass a `submit` callback to react to `failure` or to do anything beyond a redirect. When the same component renders both as a hydrated island (where `enhance` will fire) and as a plain SSR-only child (where it won't), read the [auto-injected `isHydratable` prop](/docs/environment-constants/#auto-injected-props) to skip the SSR `form`-prop peek when the client will take over. ### Submit callback Pass a function as the argument. It runs once per submit and may return a result handler that fully replaces the default fallback: ```svelte
``` The result handler receives `{ result, formElement, formData, action, update }`. Call `update({ reset?: boolean })` to re-invoke the default fallback — useful when layering extra behavior on top of it. ### onPending Pass an options object with `onPending` instead of tracking a `pending` flag inside the submit function. It fires `true` immediately before the fetch and `false` once the result handler settles (or when the submission is cancelled): ```svelte
``` `onPending` fires `false` from a `finally` block, so it resets even if the fetch throws or the abort signal fires. ### Cancelling The `submit` callback receives `cancel` and `controller`: - `cancel()` — bail out before `fetch` is issued. No callback runs. - `controller.abort()` — cancel an in-flight request. The `AbortError` is silently swallowed. ### Server-side No server changes are needed beyond declaring the action. The same `Mochi.page(path, { actions })` definition serves both the no-JS HTML POST flow and the enhanced JSON flow: ```ts // file: src/index.ts import { Mochi, fail, success } from 'mochi-framework'; await Mochi.serve({ routes: { '/login': Mochi.page('./src/Login.svelte', { actions: { default: ({ formData }) => { const username = String(formData.get('username') ?? ''); if (!username) return fail(400, { error: 'Username required' }); return success({ username }); }, }, }), }, }); ``` Returning a `Response` directly from an action bypasses the JSON envelope on enhanced submissions — treat that path as an escape hatch. **Wrap data in `success()` to round-trip it to the client.** A plain return like `return { username }` strips the data on the enhanced path and the result handler receives an empty `data` object. This matches the non-enhanced behavior. Always use `success()` when the client needs the returned data. Do **NOT** return a plain object from an action when the enhanced client needs `data`; instead, wrap it with `success()` (or `fail()` for errors). ### deserialize `deserialize(text)` decodes a raw `MochiEnhanceResult` envelope. Use it when rolling your own `onsubmit` instead of `{@attach enhance(...)}`: ```svelte ``` ### When to use enhance Reach for `enhance` when the action's outcome should update UI without a navigation flicker — interactive forms, optimistic patterns, inline validation. Stick with a plain `
` when the action ends in a redirect anyway and the JS bundle is not worth shipping. --- title: 'Why Bun?' slug: why-bun description: 'Why Mochi chose Bun as its runtime and which Bun APIs the framework relies on.' --- ## Why Bun? Mochi was built to be performant and simple. We do this by "outsourcing" subsystem complexity to the Bun runtime. Instead of writing or managing a bundler, an HTML parser, a router, database drivers, compression, and hashing as separate npm packages, the framework delegates to Bun's standard library. Bun maintains those components; Mochi just calls them. Mochi is backed by just ~10 runtime dependencies. We don't minimize dependencies for the sake of it — external depenendencies are fine when they earn their place and genuinely improve the developer experience. The point is to use Buns extensive standard library and provide an opinionated toolkit that makes it possible to build complex web apps with "batteries included" out of the box. ### What does Mochi actually use from Bun? - `Bun.build()` and `Bun.Transpiler` — Mochi's bundler. Replaces Vite and other build tools. - `Bun.plugin()` — backs the virtual `mochi` module (`isServer`, `isBrowser`, `isDev`) injected at build time. Replaces `@rollup/plugin-virtual` or a custom esbuild plugin. - `HTMLRewriter` — Mochi uses it for islands discovery and HTML rewriting. Replaces `htmlparser2`, `cheerio`, and similar libraries. - `Bun.serve()` — backs the HTTP and WebSocket server in `Mochi.serve()`. Replaces `express`, `fastify`, or `hono`. - `Bun.Glob` — discovers routes, docs, and raw CSS files. Replaces `fast-glob` or `globby`. - `Bun.deflateSync` / `Bun.inflateSync` — pack signed server-island prop payloads into URLs. Replaces `node:zlib`. - `bun:sqlite` and `bun:sql` — zero-dep SQLite and PostgreSQL for app data. Replaces `better-sqlite3` and `pg`. - `bun:test` — runs Mochi's own test suite, with per-file process isolation. Replaces Vitest or Jest. - Native `.ts` execution and auto-loaded `.env` — TypeScript runs directly under `bun run`. Replaces `ts-node` and `dotenv`. ### On the horizon As Bun gets new features, we get new abilities to extend Mochi — for example `Bun.Image()` for on-the-fly image resizing (like `next/image` without pulling in `sharp`). --- title: 'HTTP streaming' slug: http-streaming description: 'Mochi renders pages to completion before sending; SSE and WebSocket are the streaming alternatives.' --- ## HTTP streaming Mochi does not stream HTML responses. Every page renders to completion via Svelte's `render` from `svelte/server`, then ships as a single `text/html` Response. **No progressive HTML.** Slow data inside a page blocks the entire response. There is no `flushSync`, no out-of-order chunks, no `` over the network. If a page awaits a 2-second upstream call, the client waits 2 seconds before seeing any bytes. ### What does stream - `Mochi.sse(handler)` — Server-Sent Events. The response body is a `ReadableStream` and `stream.send(...)` pushes events as they happen. - `Mochi.ws(handlers)` — WebSockets via `Bun.serve` upgrade. Bidirectional, message-by-message. Use these for realtime UI on top of an already-rendered page; do **NOT** reach for them as a replacement for streamed SSR. ### Workarounds for slow pages - **Server islands** (`mochi:defer`) load slow or personalized fragments out-of-band after the shell ships. The rest of the page renders immediately; the island fetches itself once the browser sees the placeholder. - **Visible hydration** (`mochi:hydrate:visible`) keeps the initial JS payload small without progressive HTML. - **Shared HTTP cache** (Cloudflare, CloudFront, Fastly, Varnish, nginx) in front of the origin makes render time irrelevant for the cacheable common case. Server islands can stay uncached behind a cached shell — see `Cache`. Do **NOT** add an artificial loading screen to mask slow `serverProps`; instead, move the slow fetch into a `mochi:defer` island so the rest of the page paints immediately. --- title: 'Environment constants' slug: environment-constants description: 'Build-time constants for branching on render target (isServer, isBrowser) and development mode (isDev).' --- ## Environment constants Import build-time constants from the `mochi-framework` virtual module to branch on render target or dev mode: ```ts import { isServer, isBrowser, isDev } from 'mochi-framework'; ``` `mochi-framework` resolves to one of two virtual modules at compile time — server builds export `isServer = true`, client bundles export `isBrowser = true`. The values are literal booleans, so `if (isBrowser) { … }` blocks dead-code-eliminate out of the opposite bundle. Do **NOT** read `process.env.NODE_ENV` or `typeof window` to detect environment; instead, import these constants — they survive bundling intact. ### `isServer` `true` during server-side rendering, `false` in the browser. ```svelte ``` ### `isBrowser` `true` in the client bundle, `false` on the server. Use it to gate browser-only APIs (`window`, `document`, `IntersectionObserver`). ```svelte ``` ### `isDev` `true` when `Mochi.serve()` was started with `development: true`. Identical on server and client builds. ```ts // file: src/lib/log.ts import { isDev } from 'mochi-framework'; export function trace(msg: string) { if (isDev) console.log('[trace]', msg); } ``` ## Auto-injected island props The preprocessor injects one extra prop on every component invoked with `mochi:hydrate`, `mochi:hydrate:visible`, or `mochi:defer mochi:hydrate`: - `isHydratable` (`true | undefined`): `true` for hydratable invocations, absent on plain SSR-only invocations. Accept it with `$props`: ```svelte ``` For a unique per-instance id (e.g. `