Middleware
Express is a chain of functions, and a route is only the last link. Write middleware that numbers every request, logs each one with its final status, and guards two routes with an API key, and see why the order they are mounted in decides what happens.
What you'll learn
- Write middleware with (req, res, next), and pass the request on with next() or answer it instead
- Mount middleware for the whole app with app.use() or for one route in its argument list, in the order it should run
- Share a value with later handlers through res.locals, and act after the response with res.on('finish')
Every request to an Express app travels down a chain of functions, and the route handler you have been writing is only the last link. The functions before it are middleware: they can read the request, add to it, answer it outright, or pass it on. express.json() from the last lesson is one. Logging, authentication, request ids, CORS headers, rate limits: in Express, nearly everything that is not a route is middleware, and so, underneath, are NestJS's own guards and interceptors when Nest runs on Express.
(req, res, next)
A middleware is a function of three arguments:
function stamp(req: Request, res: Response, next: NextFunction) {
res.set('x-served-by', 'cats-api');
next();
}
next() hands the request to the next function in the chain. A middleware must do exactly one of two things: call next(), or answer the request itself with res. If it does neither, the request hangs: nothing later runs, and nothing answers. If it does both, the next handler answers a second time and Express complains that headers were already sent.
Where it runs: app.use() and route arguments
app.use(stamp); // every request, from here on
app.use('/admin', requireAdmin); // every request whose path starts with /admin
app.post('/owners', requireKey, create); // one route: requireKey, then create
Express runs functions in the order they were mounted. app.use() middleware mounted before the routes runs before every one of them; mounted after them, it only sees requests no route answered. That is why express.json() goes first, and why a request id assigned in the first middleware is already set when a later one refuses the request. A route can take several functions in its argument list and runs them left to right, which is how a check applies to some routes and not others.
Sharing a value: res.locals
Middleware often works something out that a handler later needs: who the user is, which request this is. res.locals is an object that lives for exactly one request and exists for this:
app.use((req, res, next) => {
res.locals.startedAt = Date.now();
next();
});
Every function after it, for the same request, can read res.locals.startedAt; the next request gets a fresh, empty res.locals.
After the response: res.on('finish')
A middleware runs before the handler, so it cannot see the status the handler will choose. It can, however, ask to be told when the response has gone out:
app.use((req, res, next) => {
res.on('finish', () => {
console.log(`${req.method} ${req.path} took ${Date.now() - res.locals.startedAt}ms`);
});
next();
});
By the time 'finish' fires, res.statusCode is final, whether a handler sent a 201 or another middleware refused with a 401. This is how access logs are written.
Refusing early
A guard is middleware that answers instead of passing on:
function requireAdmin(req: Request, res: Response, next: NextFunction) {
if (req.get('x-role') !== 'admin') {
res.status(403).json({ error: 'Admins only' });
return;
}
next();
}
The return matters: without it, the function answers and then calls next() anyway.
Your task
- Give every request an id,
req-1,req-2and so on in arrival order: put it inres.locals.requestId, and send it back in anx-request-idheader. - Record every request in
logas"<METHOD> <path> <status>", once its response has finished. - Write a middleware that lets a request through only with
x-api-key: secret-key, and otherwise answers401with{ "error": "Missing or wrong API key" }. Use it on thePOSTandDELETEroutes only. GET /loganswers the log;GET /request-idanswers{ "requestId": <this request's id> }.
When it fails
Request timed out after 5s: a middleware on the path neither callednext()nor answered.- The log shows
201where a request was refused: the status was read when the middleware ran, before anything answered. Read it inres.on('finish'). - "No key" has no
x-request-id: the id middleware is mounted after the key check. Order is the chain. - "Reading is open" answers 401: the key check was mounted with
app.use(), which applies it to every route.
Remember
- Middleware is
(req, res, next): callnext()or answer, never both, never neither. - Order of mounting is order of running;
app.use()for all routes, route arguments for one. res.localscarries values between functions for one request.res.on('finish')sees the final status.
Stuck? Show a hint
Middleware is (req, res, next) => { …; next(); }. app.use(fn) runs it for every request after it is mounted; app.post('/cats', requireKey, handler) runs requireKey before that one handler. res.statusCode is only final once the response has gone out: read it inside res.on('finish', …). A middleware that answers must not also call next().