Two ways to get dynamic data out of a URL:
- Route params — part of the path itself:
/users/42→42is the user ID - Query strings — after the
?:/users?role=admin&limit=10
The distinction matters: params identify a resource, query strings filter or modify a request.
Route parameters — req.params
We mark a dynamic segment with :name. Express captures it into req.params:
app.get('/users/:id', (req, res) => {
res.json({ userId: req.params.id });
});
// GET /users/42 → { "userId": "42" }
Multiple params work the same way:
app.get('/users/:userId/posts/:postId', (req, res) => {
const { userId, postId } = req.params;
res.json({ userId, postId });
});
// GET /users/42/posts/9 → { "userId": "42", "postId": "9" }
Gotcha #1: Params are always strings. req.params.id for /users/42 is "42", not 42. Parse with Number(req.params.id) or parseInt.
Gotcha #2: They’re URL-decoded automatically. /users/john%20doe gives req.params.id === "john doe".
Optional params
In Express 4, append ?:
// Express 4
app.get('/users/:id/posts/:postId?', (req, res) => {
if (req.params.postId) {
// single post
} else {
// all posts for user
}
});
Heads up — Express 5 changed this syntax to {} braces because they switched to path-to-regexp v8. If we’re on v5, use /users/:id/posts{/:postId}. For most production apps still on v4, the ? form works.
Wildcards
Use * for “match anything”:
app.get('/files/*', (req, res) => {
// /files/a/b/c → req.params[0] === 'a/b/c'
res.send(req.params[0]);
});
Handy for serving file paths or catch-all 404 routes.
Query strings — req.query
The bit after ? in the URL. Express parses it into an object:
app.get('/users', (req, res) => {
const { role, limit = 10 } = req.query;
res.json({ role, limit });
});
// GET /users?role=admin&limit=20
// → { "role": "admin", "limit": "20" }
Again — values are strings. limit=20 arrives as "20".
Repeated keys become arrays:
// GET /search?tag=node&tag=express
// req.query.tag === ['node', 'express']
Nested objects work too (qs library):
// GET /users?filter[role]=admin&filter[active]=true
// req.query.filter === { role: 'admin', active: 'true' }
When to use which
Think of it like this:
/users/:id— “I want the resource at this ID.” Params./users?role=admin&sort=name— “Filter/modify the result.” Query.
A common interview gotcha: “Should I use a query string or a path param for filtering?” — query. Path params identify; query params modify.
Validation reminder
req.params and req.query are user input. Validate everything. Use zod, joi, express-validator, or hand-rolled checks:
app.get('/users/:id', (req, res) => {
const id = Number(req.params.id);
if (!Number.isInteger(id) || id <= 0) {
return res.status(400).json({ error: 'Invalid id' });
}
// ... safe to query DB now
});
Skipping this is how SQL injection and NoSQL injection happen.