Documentation

Docs

Introduction

What is rune

Philosophy

Why rune

Architecture

Getting Started

Installation

Quick start

Project structure

The dev loop

Tutorial: build an app

1. Create the app

2. Pages & routes

3. Layouts

4. State with Mimir

5. SSR & data

6. API routes

7. Styling

8. Build & deploy

Core Concepts

The application object

Rendering pipeline

Hydration

The build manifest

Configuration (ekko.json)

Permissions

Conventions

Routing

File-based routing

Dynamic routes

The router (useRouter)

Navigation & Link

Guards & redirects

Programmatic routes

Server-Side Rendering

Overview

The ssr() function

Strategies (eager/lazy)

SSR → hydration

Caching & invalidation

SEO

Mimir, state management

Overview

Atoms

Reading & writing

Selectors (derived state)

Subscriptions & the store

SSR & hydration

Persistence & sessions

Patterns & recipes

Pitfalls

Pages & Layouts

Pages

Layouts

Error & not-found

API Routes

Defining routes

Request & response

Middleware

helmet

cors

rateLimit

bodyLimit

validateContentType

csrf

requestId

timeout

errorHandler

httpsRedirect

secureCookies

ipFilter

safePath

Validation & options

Styling & Theming

SCSS

Theming (light/dark)

Asgard integration

No flash (no-FOUC)

Building & Deploying

The build

Static assets

Production deploy

API Reference

ekko:rune

ekko:rune/router

ekko:rune/mimir

ekko:rune/seo

ekko:ssr / css

ekko.json schema

CLI commands

Guides

Rune app from scratch

Recipes

Dark mode

Forms

Data fetching

Authentication

Pagination

FAQ (use cases)

Documentation

Docs

Introduction

What is rune

Philosophy

Why rune

Architecture

Getting Started

Installation

Quick start

Project structure

The dev loop

Tutorial: build an app

1. Create the app

2. Pages & routes

3. Layouts

4. State with Mimir

5. SSR & data

6. API routes

7. Styling

8. Build & deploy

Core Concepts

The application object

Rendering pipeline

Hydration

The build manifest

Configuration (ekko.json)

Permissions

Conventions

Routing

File-based routing

Dynamic routes

The router (useRouter)

Navigation & Link

Guards & redirects

Programmatic routes

Server-Side Rendering

Overview

The ssr() function

Strategies (eager/lazy)

SSR → hydration

Caching & invalidation

SEO

Mimir, state management

Overview

Atoms

Reading & writing

Selectors (derived state)

Subscriptions & the store

SSR & hydration

Persistence & sessions

Patterns & recipes

Pitfalls

Pages & Layouts

Pages

Layouts

Error & not-found

API Routes

Defining routes

Request & response

Middleware

helmet

cors

rateLimit

bodyLimit

validateContentType

csrf

requestId

timeout

errorHandler

httpsRedirect

secureCookies

ipFilter

safePath

Validation & options

Styling & Theming

SCSS

Theming (light/dark)

Asgard integration

No flash (no-FOUC)

Building & Deploying

The build

Static assets

Production deploy

API Reference

ekko:rune

ekko:rune/router

ekko:rune/mimir

ekko:rune/seo

ekko:ssr / css

ekko.json schema

CLI commands

Guides

Rune app from scratch

Recipes

Dark mode

Forms

Data fetching

Authentication

Pagination

FAQ (use cases)

Request and response

API handlers receive (req, res). This page is the reference for both.

The request , req

FieldTypeWhat
req.paramsobjectPath params from :param segments (/api/notes/:id{ id }).
req.querystringThe raw query string ("?tab=a&x=1"), parse it yourself (see below).
req.pathstringThe request pathname.
req.bodyReadableStream | nullThe request body as a stream — read with getReader() to pipe a large upload without buffering.
await req.json()anyParses the body as JSON. Async, content-type-independent, single-use; throws on invalid JSON.
await req.text()stringThe body decoded as UTF-8 text.
await req.bytes()Uint8ArrayThe body as raw bytes.
await req.arrayBuffer()ArrayBufferThe body as an ArrayBuffer.
req.userobject | nullA user object, if middleware attached one.
req.headersobjectRequest headers.

Params

1
app.api("GET", "/api/notes/:id", (req) => findNote(req.params.id));

Query string

req.query is the raw string, not a parsed object. Parse it with URLSearchParams:

1
2
3
4
5
6
app.api("GET", "/api/search", async (req) => {
const params = new URLSearchParams(req.query.replace(/^\?/, ""));
const q = params.get("q") || "";
const page = Number(params.get("page") || "1");
return search(q, page);
});

This is a deliberate, documented behaviour: req.query is the raw "?k=v&k2=v2" string, not a pre-parsed object. Use URLSearchParams (or your own parser) so you control coercion and defaults.

Body

The body accessors are async — you MUST await them, and the handler must be async. This is the WHATWG Request interface (the model Deno and Bun use). req.json() returns a Promise, not the parsed value — const data = req.json() gives you a Promise (a bug); const data = await req.json() gives you the data. Each accessor is content-type-independent and single-use (call exactly one):

  • await req.json() — parse JSON (throws on invalid)
  • await req.text() — UTF-8 text
  • await req.bytes()Uint8Array
  • await req.arrayBuffer()ArrayBuffer
  • req.body — the raw ReadableStream (or null); use getReader() to stream a large upload
1
2
3
4
app.api("POST", "/api/notes", async (req) => { // handler is async
const { title, body } = await req.json(); // ← await is required
return createNote({ title, body });
});

await req.json() parses the body and throws on invalid JSON (catch it if you want to return a 400); await req.text() gives UTF-8 text; await req.bytes() / await req.arrayBuffer() give the raw bytes. req.body itself is a ReadableStream (or null when there is no body) — read it with getReader() to stream a large upload chunk-by-chunk without buffering it in memory. A body can be consumed only once.

Validate it (do not trust client input), see Validation & options.

User

If a middleware authenticates the request and sets req.user, your handler reads it:

1
2
3
4
app.api("GET", "/api/me", async (req) => {
if (!req.user) return { error: "unauthorized" };
return req.user;
});

The response , res

MethodEffect
res.json(value)Send value as application/json.
res.send(string)Send a string body.
res.status(code)Set the HTTP status (chainable / before sending).
res.header(name, value)Set a response header.
res.html(string)Send an HTML body (used by page rendering).
1
2
3
4
5
6
app.api("GET", "/api/report.csv", (_req, res) => {
res.status(200);
res.header("Content-Type", "text/csv");
res.header("Cache-Control", "no-store");
res.send("id,total\n1,42\n");
});

Return value as a shortcut

You rarely need res for JSON, return the value and rune sends it:

1
2
app.api("GET", "/api/notes", () => allNotes()); // → JSON array
app.api("GET", "/api/ping", () => "pong"); // → text

Use res directly when you need a non-default status, custom headers, or a non-JSON body.

Status code conventions

SituationStatus
Success200 (the default)
Created a resourceres.status(201)
Bad inputres.status(400)
Unauthorized / forbiddenres.status(401) / 403
Not foundres.status(404)
Wrong method on a known path405 (automatic, see Validation & options)
Unhandled throw500 (automatic)

A complete CRUD handler set

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
app.api("GET", "/api/notes", () => allNotes());
app.api("GET", "/api/notes/:id", async (req, res) => {
const n = findNote(req.params.id);
if (!n) { res.status(404); return { error: "not found" }; }
return n;
});
app.api("POST", "/api/notes", async (req, res) => {
const { title, body } = await req.json() ?? {};
if (!title) { res.status(400); return { error: "title required" }; }
const n = createNote({ title, body: body ?? "" });
res.status(201);
return n;
});
app.api("PUT", "/api/notes/:id", async (req, res) => {
const n = updateNote(req.params.id, await req.json() ?? {});
if (!n) { res.status(404); return { error: "not found" }; }
return n;
});
app.api("DELETE", "/api/notes/:id", async (req) => { deleteNote(req.params.id); return { ok: true }; });

Next: cross-cutting request handling, Middleware.