---
title: 'Welcome'
slug: intro
---
# mochi
Mochi is a SSR-focused framework for [Svelte 5](https://svelte.dev/) and [Bun](https://bun.sh/). It has a strong focus on performance and uses an islands architecture. This means you can selectively hydrate the Svelte components you want to be interactive, and leave the rest as performant HTML without having to ship the runtime for those components to the client. Less JavaScript, faster pages, and happier users are ahead!
> **Early prototype.** Only use in production if you are brave!
---
title: 'Setup'
slug: setup
---
## Setup
```sh
bun install
bun run dev # Development mode with live reload
bun run start # Production mode
```
**What to expect in the current state**
There are many things that aren't implemented yet. You'll find a non-exhaustive list below:
- HMR always does full page reloads
- No SCSS/SASS support
- No HTTP streaming (see this page for why you probably don't need it)
---
title: 'Scripts'
slug: scripts
---
## Scripts
| Script | Command | Description |
| --------- | ------------------- | ------------------------------- |
| dev | `bun run dev` | Start dev server (live reload) |
| start | `bun run start` | Start production server |
| build | `bun run build` | Pre-build islands for prod |
| typecheck | `bun run typecheck` | Type-check with tsc |
| lint | `bun run lint` | Lint with ESLint |
| lint:fix | `bun run lint:fix` | Lint and auto-fix |
| format | `bun run format` | Format with Prettier |
| clean | `bun run clean` | Remove `.mochi` build artifacts |
---
title: 'Defining routes'
slug: defining-routes
---
## Defining routes
Routes map URL paths to page entries, API handlers, WebSocket endpoints, or SSE streams:
```ts
import { Mochi } from './mochi-framework/Mochi';
const routes = {
'/': Mochi.page('./src/Home.svelte'),
'/about': Mochi.page('./src/About.svelte', { serverProps: { title: 'About' } }),
'/health': Mochi.api(({ method }) => Response.json({ status: 'ok' })),
'/ws/chat': Mochi.ws({
message(ws, msg) {
ws.send(msg);
},
}),
'/sse/time': Mochi.sse((stream) => {
stream.send('hello');
}),
};
await Mochi.serve({
port: 3333,
development: process.env.MODE === 'development',
routes,
});
```
---
title: 'Progressively enhancing forms with enhance'
slug: progressively-enhancing-forms-with-enhance
---
## Progressively enhancing forms with enhance
`enhance` is a Svelte attachment that progressively enhances a `
```
The form must live inside a hydrated island (eg: `mochi:hydrate`, `mochi:hydrate:visible`). If hydration is skipped, the attachment never runs and the form falls back to a native HTML POST automatically — that's the progressive-enhancement contract.
> Attachments require Svelte 5.29+. `enhance` is a factory: even with no options, call it as `{@attach enhance()}`.
### Wire format
`enhance` adds two headers to the POST: `Accept: application/json` and `x-mochi-action: true`. The server detects these 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 };
```
The 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 for each 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)` |
**Mochi's default fallback is intentionally lean.** Unlike SvelteKit's `use:enhance`, Mochi has no client-side `page.form` store, no `goto`, and no `invalidateAll`. The framework can't auto-update component props or re-run server data after a submission. For interactive feedback on `failure` (or anything more involved than a redirect), provide a `submit` callback.
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 }`. Calling `update({ reset?: boolean })` re-invokes the default fallback — useful when you only want to layer extra behavior on top of the framework default.
### onPending
Instead of managing a `pending` flag inside the submit function, pass an options object with `onPending`. The callback fires with `true` immediately before the fetch and `false` once the result handler has settled (or when the submission is cancelled):
```svelte
```
`onPending` is guaranteed to fire `false` in 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 the `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
import { fail, redirect, success } from 'mochi-framework';
Mochi.page('./Login.svelte', {
actions: {
default: ({ formData, cookies }) => {
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.** If an action returns a plain object (e.g. `return { username }`) instead of `return success({ username })`, the enhanced path replaces the data with `{}`. This matches the non-enhanced behavior and is not a regression, but it means your result handler will receive an empty `data` object. Always use `success()` when the client needs the returned data.
### 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 messages. Stick with a plain `