Middleware
before / after / wrap hooks, composed on the hopak() server.
Three hooks for the request pipeline. Typed functions, not a Koa-style (ctx, next) chain — no next() to forget.
type Before = (ctx: RequestContext) =>
Promise<Response | void> | Response | void;
type After = (
ctx: RequestContext,
result: { response?: Response; error?: unknown },
) => Promise<void> | void;
type Wrap = (
ctx: RequestContext,
run: () => Promise<Response>,
) => Promise<Response>;
Before— runs before the handler. Throw aHopakErrorto short-circuit with that status. Return aResponseto short-circuit with that response. Return nothing to continue. Mutations toctxflow through (e.g.ctx.user = ...). Right place for auth, rate-limiting, request-id.After— runs after the handler (or error), with the final response. Cannot change the response — read-only. Use for access logs, metrics, audit trails. If it throws, the error is logged and the request still completes.Wrap— wraps handler execution (plus route-levelbefores).run()produces the response. Use this only when observation isn’t enough — per-request transactions, request-scoped caches, correlation-id propagation inasync_hooks.
Where to register
Two scopes — global (every request) and per-route:
// main.ts — global
import { hopak, requestId, requestLog } from '@hopak/core';
await hopak()
.before(requestId())
.after(requestLog())
.wrap(async (_ctx, run) => run()) // rarely needed
.listen();
// app/routes/api/posts.ts — per-route
export const POST = defineRoute({
before: [requireAuth()],
after: [audit],
handler: async (ctx) => { /* … */ },
});
crud.* helpers take the same options as a second argument —
see CRUD → Gate a CRUD verb.
Execution order
For one request:
global.before[] → wrap[] → route.before[] → handler
(throw or return Response short-circuits)
route.after[] → global.after[]
Wraps nest — the outer-most runs first on entry, last on exit
(like onion layers).
hopak().before/.after/.wrap is frozen after listen()
Registering middleware after the server starts throws:
const app = hopak();
await app.listen();
app.before(requestId());
// Error: hopak().before(): cannot register middleware
// after listen() — add it before starting the server.
This protects against half-applied middleware on live requests.
Built-in: requestId() + requestLog()
See Recipe 23 for the full walkthrough. One command enables both:
hopak use request-log.
EMPTY_MIDDLEWARE
Exported sentinel for { before: [], after: [], wrap: [] }. Useful
if you compose your own Middleware object and want an explicit
empty default.