3.2 KiB
middleware
(no submodules)
Purpose
The middleware module defines the onion-model Middleware contract and
the composeMiddleware builder. Every adapter consumes the same shape so
middleware authored against the abstract contract drops into every router
implementation (Hono, in-memory, the worker-bridge router) unchanged.
The contract is intentionally narrow: a middleware receives the request
and a next() continuation, and must return a response. There is no
ctx mutation handle, no implicit state — middleware threads explicit
values via wrapping responses and inspecting the request shape.
Features
Middleware<ReqBody, ResBody>— type generic over the request body and response body shapes so handler-level types flow through.composeMiddleware([…])— builds a single composed middleware from a list; order is preserved.next()enforcement: a middleware that callsnext()twice in a row has its dispatch rejected. (Multiple sequentialnext()calls across different middlewares are correct; only same-middleware repeats fail.)- Synchronous return values are accepted (wrapped in
Promise.resolve).
Usage: Basic
import { composeMiddleware } from "@suaveplan/api-router/middleware";
const log = composeMiddleware([
async (req, next) => {
const res = await next();
return res; // pass-through with hook points
},
]);
Usage: Advanced
import { composeMiddleware, type Middleware } from "@suaveplan/api-router/middleware";
const auth: Middleware = async (req, next) => {
if (req.headers.get("authorization") === null) {
return { status: 401, headers: {}, body: { code: "UNAUTH" }, streaming: false };
}
return next();
};
const trace: Middleware = async (req, next) => {
req.span.setAttribute("path", req.url);
const res = await next();
req.span.setAttribute("status", res.status);
return res;
};
const stack = composeMiddleware([trace, auth]);
// Execution: trace → auth → terminal → auth → trace
API Reference
Middleware<ReqBody, ResBody>—(req, next) => RouterResponse | Promise<RouterResponse>.composeMiddleware(list)— returns a single composedMiddleware.PipelineStep— literal union of every per-request pipeline step name.PLUGIN_INSERTION_STEP— the constant"post-validation"; the single insertion point plugin middleware is allowed.assertPluginInsertionStep(step)— throwsROUTER.MIDDLEWARE_INSERTION_DENIEDifstepisn't the allowed boundary.
Implementation Notes
composeMiddleware uses the textbook indexed-dispatch pattern: each call
to next() advances an internal index, and double-next() from the same
middleware is detected by comparing the new index to the recorded
high-watermark. The detection rejects the inner Promise rather than
throwing synchronously so the error propagates through the caller's
existing .catch / try/await machinery.
Synchronous middlewares are supported — the return value is wrapped in
Promise.resolve so callers don't have to choose between async and
plain functions. The terminal continuation is invoked with no arguments
and must produce the response; adapters wire this to the handler call.