Middleware and input
Compose middleware
Use small ordered middleware for cross-cutting behavior without turning request flow into a maze.
Middleware can do four things to a request: observe it, change it, stop it, or forward it. Every middleware you write should do one of those, and you should be able to say which. When I can’t, the function is doing too much.
The other decision is scope. A request id and a logger belong to every request. A check that the current user owns a note belongs to the notes router and nowhere else. Mount each one at the narrowest place that still covers what it needs.
Global: observe and change
The request id middleware changes the request by attaching an id, then forwards:
import { randomUUID } from 'node:crypto'
export function requestId(req, res, next) {
req.id = req.get('x-request-id') ?? randomUUID()
res.set('x-request-id', req.id)
next()
}
The logger observes. It records after the response finishes, so it can see the status and the duration:
export function logger(req, res, next) {
const start = performance.now()
res.on('finish', () => {
const ms = Math.round(performance.now() - start)
console.log(`${req.id} ${req.method} ${req.originalUrl} ${res.statusCode} ${ms}ms`)
})
next()
}
Both go first in createApp(), before anything that could send a response:
app.use(requestId)
app.use(logger)
Scoped: stop or forward
Checking that a visitor is logged in either stops the request or lets it through:
export function requireLogin(req, res, next) {
if (!req.session.userId) {
return res.redirect(303, '/login')
}
next()
}
The notes router needs it. The login page and the stylesheet do not. So it goes on the mount, not on the app:
app.use('/notes', requireLogin, notesRouter)
app.use() accepts several functions in a row, and runs them in order for that path only. Requests for /login never see requireLogin, which is what we want. A global requireLogin would redirect the login page to itself.
Write the order down
For the notes app, the full chain for a POST /notes/42 looks like this:
requestId, changesreqlogger, observesexpress.static()for/assets, stops or forwardsrequireLogin, stops or forwardsexpress.urlencoded(), changesreq.bodyrequireOwner, stops or forwards- the route handler, stops
- error middleware, only if something threw
Two things fall out of this list. Parsing runs after the login check, so anonymous requests never cost us a body parse. And ownership runs after parsing, because it may need the body. If I ever need a rate limiter, it goes between 2 and 3, before any real work.
The test that proves the scope
Predict what happens for GET /login with no session, then check:
test('the login page does not require a login', async () => {
const res = await request(app).get('/login')
assert.equal(res.status, 200)
})
If someone later moves requireLogin to app.use() for convenience, this test turns red. That’s a middleware scope bug caught in a second instead of in a bug report.
Try this: change the order of requireLogin and express.urlencoded() and confirm that an anonymous POST is now parsed before being rejected. Then put them back.
Lesson completed