Middleware
Overview
Middleware functions sit between the incoming HTTP request and your route handler. They form an onion-shaped pipeline where each layer can inspect, modify, or short-circuit the request/response.
Signature
import type { Middleware, NextFunction } from "@rune/core";
const myMiddleware: Middleware = async (ctx, next) => { // before handler await next(); // after handler};Registration
import { createApp } from "@rune/core";
const app = createApp();
app.use(async (ctx, next) => { console.log(`${ctx.request.method} ${ctx.request.url}`); await next();});Order of app.use() calls determines execution order. First registered = outermost layer.
Context API
The ctx argument is a Context instance with:
ctx.request // Web-standard Request objectctx.params // Route path parametersctx.query // Parsed query stringctx.headers // Request headersctx.body // Parsed JSON body (Promise)ctx.state // Mutable Map shared across middlewarectx.container // Request-scoped DI containerctx.response // Response object (set by middleware)ctx.send(data, status?) // Set JSON responsectx.sendStatus(status) // Set status-only responseCommon Patterns
Logger Middleware
app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; console.log(`${ctx.request.method} ${ctx.request.url} - ${ms}ms`);});CORS Middleware
app.use(async (ctx, next) => { const response = await next(); if (response) { const headers = new Headers(response.headers); headers.set("access-control-allow-origin", "*"); headers.set("access-control-allow-methods", "GET, POST, PUT, DELETE, OPTIONS"); headers.set("access-control-allow-headers", "Content-Type, Authorization"); return new Response(response.body, { status: response.status, headers, }); }});Auth Middleware
app.use(async (ctx, next) => { const auth = ctx.request.headers.get("authorization"); if (!auth?.startsWith("Bearer ")) { return new Response("Unauthorized", { status: 401 }); } const token = auth.slice(7); ctx.state.set("user", await verifyToken(token)); await next();});Error Handler Middleware
app.use(async (_ctx, next) => { try { await next(); } catch (err) { return new Response( JSON.stringify({ error: err instanceof Error ? err.message : "Internal error" }), { status: 500, headers: { "content-type": "application/json" } }, ); }});Request ID Middleware
app.use(async (ctx, next) => { const id = crypto.randomUUID(); ctx.state.set("requestId", id); const response = await next(); if (response) { const headers = new Headers(response.headers); headers.set("x-request-id", id); return new Response(response.body, { status: response.status, headers }); }});Response Time Header
app.use(async (ctx, next) => { const start = performance.now(); const response = await next(); const elapsed = (performance.now() - start).toFixed(2); if (response) { const headers = new Headers(response.headers); headers.set("x-response-time", `${elapsed}ms`); return new Response(response.body, { status: response.status, headers }); }});Short-Circuiting
Return a Response directly from middleware to skip the handler and all inner layers:
app.use(async (ctx) => { if (ctx.request.method === "OPTIONS") { return new Response(null, { status: 204 }); } // Fall through to next middleware const { next } = ctx.state.get("__pipeline") ?? {}; return next ? await next() : undefined;});Middleware Composition
For reusable middleware groups:
import { MiddlewarePipeline } from "@rune/core";
function authMiddleware(): Middleware[] { return [ async (ctx, next) => { /* check JWT */ await next(); }, async (ctx, next) => { /* check roles */ await next(); }, ];}
const pipeline = new MiddlewarePipeline();pipeline.use(...authMiddleware());app.use(pipeline.compose(async (ctx) => ctx.send("ok")));