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)

Validation and options

This page covers the rest of the API surface: per-route options, the automatic 405 Method Not Allowed, input validation, and body limits.

Per-route options

app.api(method, path, opts, handler) accepts an options object between the path and the handler. It is passed to the underlying server registration, so it is where transport-level settings live (for example body size limits for a specific upload endpoint):

1
2
3
app.api("POST", "/api/upload", { maxBodySize: 10 * 1024 * 1024 }, async (req, res) => {
// accept up to 10 MB on this route
});

Omit opts for the common case:

1
app.api("GET", "/api/notes", () => allNotes());

Automatic 405 Method Not Allowed

When you register one or more methods for a path, rune automatically answers other methods to that path with 405 and an allowed list, you do not write that handler:

1
2
3
app.api("GET", "/api/notes", () => allNotes());
app.api("POST", "/api/notes", async (req) => createNote(await req.json()));
// PUT /api/notes → 405 { error: "Method Not Allowed", allowed: ["GET","POST"] }

This keeps your API honest (a wrong method gets a correct 405, not a confusing 404) with zero extra code.

Validate input , do not trust the client

await req.json() parses whatever the client sent (content-type-independent, single-use). Validate it in the handler before using it. A simple guard:

1
2
3
4
5
6
7
8
9
10
11
12
app.api("POST", "/api/notes", async (req, res) => {
const b = await req.json();
if (!b || typeof b.title !== "string" || b.title.length === 0) {
res.status(400);
return { error: "title is required and must be a non-empty string" };
}
if (b.body != null && typeof b.body !== "string") {
res.status(400);
return { error: "body must be a string" };
}
return createNote({ title: b.title.slice(0, 200), body: (b.body ?? "").slice(0, 10000) });
});

For larger schemas, write a small validator module (or use a schema library you ship) and run it at the top of each handler. The principle is the same: reject bad input with a 400 and a clear message; never pass unvalidated input to your data layer.

A reusable validator

1
2
3
4
5
6
// lib/validate.ts
export function requireFields(body: any, fields: string[]): string | null {
if (!body || typeof body !== "object") return "body must be an object";
for (const f of fields) if (body[f] == null) return `${f} is required`;
return null;
}
1
2
3
4
5
6
app.api("POST", "/api/notes", async (req, res) => {
const body = await req.json();
const err = requireFields(body, ["title"]);
if (err) { res.status(400); return { error: err }; }
return createNote(body);
});

Body size limits

Two levels:

  • App-wide: createApp({ maxBodySize, maxWsMessageSize }) sets defaults for the whole server.
  • Per-route: the opts argument (above) overrides for one endpoint.

Set conservative limits, an unbounded body is a denial-of-service vector. Raise it only for endpoints that genuinely need large payloads (uploads), and validate the content there.

Permissions and the API

API handlers run under the same permission grant as the rest of the app. If a handler reads files, the app needs fs; if it calls an upstream service, it needs net. Scope these in ekko.json so a handler can only reach what it should:

1
2
3
4
5
"permissions": {
"fs": ["./data/**"], // the API writes data/notes.json, nothing else
"net": ["api.upstream.com"], // and only talks to one host
"env": ["DATABASE_URL"]
}

Idempotency and methods

Follow HTTP semantics so clients, caches, and the automatic 405 behave predictably:

  • GET , read, no side effects, cacheable.
  • POST , create / non-idempotent action.
  • PUT , replace a resource (idempotent).
  • PATCH , partial update.
  • DELETE , remove (idempotent).

That completes API Routes. Next: styling the app, Styling → SCSS.