🍡 mochi

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

API routes

Mochi.api() creates JSON API endpoints. The handler receives a MochiApiEvent with method, request, url, and locals:

import { error } from "./mochi-framework/utils";

"/health": Mochi.api(({ method }) =>
  Response.json({ status: "ok", method }),
),

"/add": Mochi.api(async ({ method, request }) => {
  if (method !== "POST") error(405, "Method Not Allowed");
  const { a, b } = await request.json();
  return Response.json({ result: a + b });
}),

Error responses

API routes return a standard JSON envelope for all errors — they never render the HTML error page, and handleError is not called for them. Use apiError(status, message) to build the envelope response:

import { apiError } from 'mochi-framework';

Mochi.api(async ({ request }) => {
  const body = await request.json().catch(() => null);
  if (!body) return apiError(400, 'Invalid JSON');
  return Response.json({ ok: true });
});
// bad body → 400 { "error": { "message": "Invalid JSON", "status": 400 } }

apiError returns a Response with Content-Type: application/json and the HTTP status set to match. Prefer it over throwing when the error is part of the route’s normal control flow.

Thrown MochiHttpError — same envelope. Use error(status, message) from mochi-framework to throw one from deep inside helper code where returning is awkward:

import { error } from 'mochi-framework';

error(404, 'Not found');
// → 404 { "error": { "message": "Not found", "status": 404 } }

Any other uncaught throw — returned as 500 Internal Server Error with a generic message. The original error (including stack) is logged server-side via log.error with the method and path; it is not leaked to the client. Throw MochiHttpError (or return apiError(…)) explicitly when you want a specific status or message to reach the caller.

Mochi.api(async () => {
  await db.query('SELECT …'); // throws ConnectionError
  return Response.json({ ok: true });
});
// → 500 { "error": { "message": "Internal Server Error", "status": 500 } }
// (real error + stack logged to stderr)