The same function can be attached at several scopes, and the scope decides how many requests pay for it. Bind at the narrowest scope that still covers every request needing the behavior: a body parser mounted globally parses JSON for your health check too.
app.use(requestTimer); // application: every request
app.use('/api', requireApiKey({ keys })); // application, path-mounted
const books = express.Router();
books.use(express.json({ limit: '32kb' })); // router: every route in this router
books.param('id', (req, res, next, id) => { // router: only routes with :id
req.bookId = Number(id);
next();
});
books.post('/', validateBook, createBook); // route: this one endpoint
app.use('/api/books', books);| Scope | Registered with | Runs for | Typical use |
|---|---|---|---|
| Application | app.use(fn) | Every request | Logging, request id, helmet 10,736 |
| Path-mounted | app.use('/api', fn) | Paths under the prefix | API keys, CORS for one area |
| Router | router.use(fn) | Routes in that router | Body parsing, auth for a resource |
| Parameter | router.param('id', fn) | Routes containing :id | Loading a record once |
| Route | app.get(p, fn, handler) | One method and path | Validation, upload handling |
router.param is the scope people forget. Registering it once replaces the "parse the id, look it up, 404 if missing" preamble in every handler that takes an :id, it runs before any route-level middleware on the matched route, and it fires only once per request even when two layers match the same parameter.
Mount paths are stripped, not just matched. Inside a router mounted at /api/books, req.url is /42 while req.originalUrl is /api/books/42, and req.baseUrl holds the prefix. Log req.originalUrl; route on req.url.