🍡 mochi

SSR framework for Svelte 5 + Bun with islands-based selective hydration

On this page

Utility helpers

Functions exported from mochi-framework for shaping responses and form-action results. Each helper is documented in depth where it is used. This page is an index.

Response helpers

json(data, init?) builds a JSON Response with the right Content-Type. Use it from Mochi.api() handlers and middleware.

import { json } from 'mochi-framework';

return json({ ok: true }, { status: 201 });

error(status, message?) throws a MochiHttpError that the framework catches and renders as the configured error page (or a JSON envelope from API routes). Omit message to get the canonical status text — Not Found, Too Many Requests, … — with unknown codes falling back to Error <status>. See Error handling.

Since: 0.10.0 (not released yet): Calling error(status) without a message requires mochi-framework 0.10.0.

import { error } from 'mochi-framework';

if (!user) error(404, 'User not found');
if (!session) error(401); //  "Unauthorized"

apiError(status, message) returns a JSON error Response shaped as { error: { message, status } }. Use it inside Mochi.api() for a typed error without unwinding the stack. See API routes.

import { apiError } from 'mochi-framework';

return apiError(400, 'Missing id');

Form-action helpers

Use these as return values from a Mochi.page action. See Defining routes and Progressive enhancement for the full action lifecycle.

fail(status, data) re-renders the page with form = { ok: false, action, status, data } and the given HTTP status. The payload nests under form.data, not on form itself. Use it for validation errors.

import { fail } from 'mochi-framework';

if (!username) return fail(400, { error: 'Username required', username });
{#if form?.data?.error}<p>{form.data.error}</p>{/if}

success(data?) re-renders the page with form = { ok: true, action, data } and HTTP 200. Use it when the action completes and you want to stay on the page.

import { success } from 'mochi-framework';

return success({ message: 'Saved.' });

redirect(status, location) issues an HTTP redirect when returned from a form action or a serverProps resolver. status must be 301, 302, 303, 307, or 308. Use 303 for the standard POST/Redirect/GET pattern.

import { redirect } from 'mochi-framework';

return redirect(303, '/dashboard');

Sealed tokens

encryptPayload(plaintext, { aad?, compress? }) and decryptPayload(token, { aad? }) seal a string into an opaque, tamper-proof base64url token (AES-256-SIV keyed from MOCHI_KEY) and open it again. decryptPayload returns null on any tamper or aad mismatch. This is the same primitive Mochi uses for server-island props. Use it for short-lived signed values such as form challenges or magic links.

import { encryptPayload, decryptPayload } from 'mochi-framework';

const token = encryptPayload(JSON.stringify({ iat: Date.now() }), { aad: 'my-form' });
const opened = decryptPayload(token, { aad: 'my-form' }); // string | null

HTML escaping

Since: 0.10.0 (not released yet): escapeHtmlAttr became a public export in 0.10.0.

escapeHtmlAttr(value) replaces &, ", < and > with entities. It is the framework’s single attribute encoder, so values round-trip exactly through getAttribute() — including payloads that already contain entity sequences like ", which a bare "-only replace would corrupt. It works for text content too.

import { escapeHtmlAttr } from 'mochi-framework';

const html = `<pre><code>${escapeHtmlAttr(source)}</code></pre>`;

It has zero imports on purpose, so server modules, SSR, hydrated components and client-bundled web components all share the one implementation.

Process singletons

pinGlobal(key, factory) returns one instance per key for the life of the process, pinned on globalThis. The factory runs at most once per key; every later call with the same key returns the same value.

import { SQL } from 'bun';
import { pinGlobal } from 'mochi-framework';

export function getSql() {
  return pinGlobal('app:sql', () => new SQL(process.env.DATABASE_URL!, { max: 8 }));
}

Call the accessor wherever you need the value — every call returns the same instance:

import { getSql } from './db/client';

const users = await getSql()`SELECT id, name FROM users`;

Reach for it to hold anything that must not be duplicated: a database pool, a connection, a background timer, an in-memory cache. In development this is what keeps such resources from leaking across route-handler HMR — a plain module-scoped let is re-created on every reload and its old handle orphaned, but a pinGlobal value survives. Namespace your key with an app: prefix; the framework pins its own state under __mochi_*__.

See it in action

Live demos showing key concepts from this page