InteractiveFrameworks

Middleware

Run code around every handler: a middleware that tags each request with an id the handler can read, and one that guards the cats routes with an API key and answers 401 itself.

What you'll learn

  • Write middleware with createMiddleware(): code before await next() runs before the handler, code after it runs after
  • Pass a value from middleware to the handler with c.set() and c.get(), typed through the app's Variables
  • Scope middleware to a path with app.use('/cats/*', …) and end a request early by returning a response

Some work belongs to every request rather than to one route: tagging it with an id so a log line can be traced, measuring how long it took, checking a key before anything under /cats is touched. Copying that into each handler of the cats API would be wrong twice over, once per copy and once more when a route is added without it. Hono's answer is middleware: a function that runs around the handler, registered once for a path pattern.

What middleware is

A middleware has the same shape as a handler, with a second argument:

app.use(async (c, next) => {
  console.log(`[${c.req.method}] ${c.req.url}`);
  await next();
});

next() runs whatever comes after this middleware, the next middleware or the handler, and resolves when that has produced its response. Code before await next() runs before the handler; code after it runs after, when c.res is the response about to be sent. A middleware ends in one of two ways: it awaits next() and returns nothing, or it returns a response of its own and the handler never runs. It must do one or the other. A middleware that does neither leaves Hono with nothing to send, and the console shows Error: Context is not finalized. Did you forget to return a Response object or await next()? with a 500.

The docs draw this as an onion. Register three middleware and a handler and the order of execution is:

middleware 1 start
  middleware 2 start
    middleware 3 start
      handler
    middleware 3 end
  middleware 2 end
middleware 1 end

Registration order is execution order, and middleware must be registered above the routes it should wrap, because a request only meets what was registered before the route that answers it.

Where it applies

app.use(fn) applies to every path. app.use('/posts/*', fn) applies under /posts, including /posts itself, and app.post('/posts/*', fn) to one verb. A path pattern in app.use follows the same rules as a route's: parameters, wildcards, exact segments.

Changing the response after next()

After await next(), the response exists. c.res.status is its status, c.res.headers its headers, and c.header() still works: Hono re-creates the response with the header added.

app.use(async (c, next) => {
  const started = Date.now();
  await next();
  c.header('x-response-time', `${Date.now() - started}ms`);
});

Passing values to the handler

Middleware and handlers share the context, and c.set(key, value) / c.get(key) carry a value from one to the other for the length of one request. The keys are typed: an app declares its variables and TypeScript refuses any other key.

type Env = { Variables: { user: string } };
const app = new Hono<Env>();

app.use(async (c, next) => {
  c.set('user', c.req.header('x-user') ?? 'anonymous');
  await next();
});

app.get('/whoami', (c) => c.text(c.get('user')));

Without the Env generic, c.set('user', …) is a type error on the key, because a bare new Hono() declares no variables. That is the compiler telling you the app and its middleware must agree on what travels on the context.

Middleware in its own file

Written inline in app.use, a middleware cannot be reused or tested on its own. createMiddleware from hono/factory gives a standalone function the right types:

import { createMiddleware } from 'hono/factory';

export const timing = createMiddleware(async (c, next) => {
  const started = Date.now();
  await next();
  c.header('x-response-time', `${Date.now() - started}ms`);
});

It takes the same Env generic, createMiddleware<Env>(…), so c.set inside it is typed like the app's.

Where it sits

Everything Hono does to a request is this one chain: middleware before, the handler, middleware after. A guard is a middleware that returns early; an interceptor is one that reads c.res after next().

Your task

The cats API has two middleware stubs in middleware.ts that only call next().

  1. requestId takes the id from the x-request-id request header, or makes one up with crypto.randomUUID(), keeps it on the context under requestId, lets the handler run, then adds two response headers: x-request-id with the id and x-status with the status the response ended up with.
  2. apiKey refuses any request whose x-api-key header is not API_KEY, answering 401 with { "error": "Missing or invalid API key" } without running the handler.
  3. In main.ts, apply requestId to every route and apiKey to /cats and everything under it. GET / stays public.
  4. GET /cats/:id answers the cat with the request id added: { …cat, "requestId": "…" }.

When it fails

  • GET / answers 500 and the console says Context is not finalized: a middleware neither called next() nor returned a response. Every path through a middleware must do one of the two.
  • x-status is 200 on a 404, or missing: the header was set before await next(), when the response did not exist yet. Read c.res.status after it.
  • GET / asks for an API key: apiKey was registered with '*' or with no path. Scope it: app.use('/cats/*', apiKey).
  • GET /cats with the key still answers 401: the guard compared against the wrong header name, or was registered after the routes, so the route answered first and the guard never ran. Middleware goes above the routes.
  • c.set('requestId', …) is a type error: the app or the middleware was created without the Env generic.

Remember

  • Middleware runs around the handler: before await next(), then after, in registration order, registered above the routes.
  • It must either await next() or return a response; doing neither is a 500.
  • c.set() and c.get() share a value for one request, typed through the app's Variables.
  • createMiddleware<Env>() from hono/factory makes a reusable, typed middleware; app.use('/path/*', …) scopes it.
Stuck? Show a hint

createMiddleware<Env>(async (c, next) => { … }) gives c and next their types. Read the incoming id with c.req.header('x-request-id'), keep it with c.set, and after await next() the response exists: c.res.status is its status and c.header() still adds to it. The guard compares c.req.header('x-api-key') with the expected key and returns c.json(…, 401) instead of calling next().