Route Parameters
Most useful URLs aren’t static — /users/42 and /users/99 should hit the same handler but operate on different records. Route parameters let you capture those dynamic segments of a path by name, so a single route definition can serve an entire family of URLs. Express parses the matched values out of the URL and hands them to you on req.params, ready to drive a database lookup or any other logic.
The :param syntax
You mark a path segment as dynamic by prefixing it with a colon. The name after the colon becomes the key under which Express stores the captured value:
const express = require("express");
const app = express();
app.get("/users/:id", (req, res) => {
res.json({ userId: req.params.id });
});
app.listen(3000);
The path /users/:id matches any single-segment URL of the form /users/<something>. When a request arrives, Express splits the URL, lines it up against the pattern, and fills in req.params with the matched piece.
A request for GET /users/42 produces:
Output:
{ "userId": "42" }
Note: Every value in
req.paramsis a string, even when it looks like a number. If you need a real number, convert it explicitly withNumber(req.params.id)orparseInt(req.params.id, 10)and validate the result.
Multiple parameters
A path can contain as many named parameters as you need, each separated by a literal segment. This is the natural way to express nested or hierarchical resources:
app.get("/users/:userId/posts/:postId", (req, res) => {
const { userId, postId } = req.params;
res.json({ userId, postId });
});
A request for GET /users/7/posts/13 returns:
Output:
{ "userId": "7", "postId": "13" }
Destructuring req.params keeps handlers readable once more than one parameter is involved. Parameter names must be made up of word characters ([A-Za-z0-9_]), and each name has to be unique within a single route.
A realistic /users/:id example
In practice a parameter feeds a lookup. Here an async handler awaits a database call, validates the input, and responds with the right status code:
app.get("/users/:id", async (req, res, next) => {
const id = Number(req.params.id);
if (!Number.isInteger(id)) {
return res.status(400).json({ error: "id must be an integer" });
}
try {
const user = await db.findUser(id);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
res.json(user);
} catch (err) {
next(err);
}
});
This pattern — parse, validate, look up, respond — covers the vast majority of parameterized routes.
Optional parameters
Sometimes a segment may or may not be present. The syntax for optional parameters differs between Express versions, which is a common source of confusion:
| Version | Optional syntax | Example |
|---|---|---|
| Express 4.x | trailing ? | /users/:id? |
| Express 5.x | braces {} | /users{/:id} |
In Express 5, route matching moved to path-to-regexp v8, where the ? suffix was removed in favor of wrapping the optional part in braces:
// Express 4.x — :format is optional
app.get("/report/:year/:format?", (req, res) => {
res.json({ year: req.params.year, format: req.params.format ?? "html" });
});
// Express 5.x — equivalent route
app.get("/report/:year{/:format}", (req, res) => {
res.json({ year: req.params.year, format: req.params.format ?? "html" });
});
When the optional segment is absent, its key is simply undefined on req.params, so supply a fallback (here with ??) where it matters.
Constraining parameters with patterns
You often want a parameter to match only certain values — digits, for instance. In Express 4 you can attach an inline regular expression to a parameter directly in the path:
// Express 4.x — :id must be one or more digits
app.get("/users/:id(\\d+)", (req, res) => {
res.json({ id: Number(req.params.id) });
});
Now /users/42 matches but /users/abc falls through to the next route (typically a 404). Inline parameter regexes were removed in Express 5; the recommended replacement is to validate inside the handler, or to centralize the check with app.param:
// Works in Express 4 and 5 — runs before any route using :id
app.param("id", (req, res, next, value) => {
if (!/^\d+$/.test(value)) {
return res.status(400).json({ error: "id must be numeric" });
}
req.userId = Number(value);
next();
});
app.get("/users/:id", (req, res) => {
res.json({ id: req.userId });
});
app.param registers a trigger that fires whenever a route with that named parameter matches, letting you validate or pre-load a resource in one place instead of repeating the logic across handlers.
Tip: Reserve literal routes like
/users/mefor before their parameterized sibling/users/:id. Because the first matching route wins, defining/users/:idfirst would capturemeas anidand shadow the literal route.
Best Practices
- Treat every
req.paramsvalue as a string and convert/validate it before use — never trust raw URL input. - Use clear, singular parameter names (
:userId,:postId) that describe the resource the segment identifies. - Validate parameters early and return
400for malformed input,404for valid-but-missing resources. - Centralize repeated parameter validation or resource loading with
app.paramrather than duplicating it in each handler. - Register specific literal routes before parameterized ones so the dynamic pattern doesn’t swallow them.
- Be explicit about Express version when using optional (
?vs{}) or pattern ((\\d+)) syntax — the rules changed in 5.x.