Files
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')
Middleware
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().
Press Run tests to start the app. Its log appears here.Graded endpoints
No key needed to read, and the first request gets the first id
The key check answers before the handler runs; the id is still set, because that middleware came first
Only the one key opens the route
The check calls next(), and the handler runs
The same check guards the DELETE route
Through the check, and the cat is gone
Through the check again, to the handler's own 404
Tom and Luna, and the eighth id
A value set by middleware, read by the handler for the same request
Every request so far, each with the status it finally got, recorded after its response finished