Files
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
Middleware
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().
requestIdtakes the id from thex-request-idrequest header, or makes one up withcrypto.randomUUID(), keeps it on the context underrequestId, lets the handler run, then adds two response headers:x-request-idwith the id andx-statuswith the status the response ended up with.apiKeyrefuses any request whosex-api-keyheader is notAPI_KEY, answering401with{ "error": "Missing or invalid API key" }without running the handler.- In
main.ts, applyrequestIdto every route andapiKeyto/catsand everything under it.GET /stays public. GET /cats/:idanswers the cat with the request id added:{ …cat, "requestId": "…" }.
When it fails
GET /answers500and the console saysContext is not finalized: a middleware neither callednext()nor returned a response. Every path through a middleware must do one of the two.x-statusis200on a 404, or missing: the header was set beforeawait next(), when the response did not exist yet. Readc.res.statusafter it.GET /asks for an API key:apiKeywas registered with'*'or with no path. Scope it:app.use('/cats/*', apiKey).GET /catswith 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 theEnvgeneric.
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()andc.get()share a value for one request, typed through the app'sVariables.createMiddleware<Env>()fromhono/factorymakes 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().
Press Run tests to start the app. Its log appears here.Graded endpoints
The request id middleware runs for every route: the id sent in is echoed back, and x-status is set after next() from the response
The guard on /cats/* answers 401 itself and never calls next(); the outer middleware still tags the response
A key that does not match is refused the same way
The guard calls next() and the handler answers
The handler gets the request id from c.get(), set by the middleware that ran before it
The handler's own 404 passes back through the middleware, which reads its status
The guard is scoped to /cats/*, so the root does not ask for a key