Docs / Middleware

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>;

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.