A colon in a path pattern captures one path segment into req.params under that name. Parameter values are always strings, always URL-decoded, and never span a /: :bookId in /books/:bookId matches 42 but not 42/reviews. Convert to a number yourself, and validate before converting — Number('') is 0, exactly the kind of bug that lets a malformed URL reach your database.
Express 5 24,430 requires parameter names to be valid JavaScript identifiers; a name with a hyphen must be quoted, as :"tag-name". Two parameters can share one segment as long as a literal character separates them, which is how /flights/LAX-SFO splits into an origin and a destination.
app.param(name, fn) registers a callback that runs once per request, before any handler whose path contains that parameter. It is the natural home for the load-or-404 step, and it runs only when the parameter is present, so it costs nothing on unrelated routes.
import express from 'express';
const app = express();
app.param('bookId', (req, res, next, value) => {
if (!/^\d+$/.test(value)) return res.status(400).json({ error: 'bookId must be numeric' });
req.book = { id: Number(value), title: `Book ${value}` };
next();
});
app.get('/books/:bookId', (req, res) => res.json({ params: req.params, book: req.book }));
app.get('/books/:bookId/reviews/:reviewId', (req, res) => res.json(req.params));
app.get('/flights/:from-:to', (req, res) => res.json(req.params));
app.get('/tags/:"tag-name"', (req, res) => res.json(req.params));GET /books/42 -> 200 {"params":{"bookId":"42"},"book":{"id":42,"title":"Book 42"}}
GET /books/42/reviews/7 -> 200 {"bookId":"42","reviewId":"7"}
GET /books/abc -> 400 {"error":"bookId must be numeric"}
GET /flights/LAX-SFO -> 200 {"from":"LAX","to":"SFO"}
GET /tags/web%20dev -> 200 {"tag-name":"web dev"}Notice the last line: the request asked for web%20dev and req.params['tag-name'] came back as web dev. That decoding has a sharp edge. An invalid escape such as %zz makes Express reject the request with a 400 before your handler runs, and %2F decodes to a slash after matching, so a single parameter can hold a value that looks like two segments. Never interpolate one into a filesystem path or a shell command unchecked.