Route parameters and query strings in Express?
Route parameters and query strings are two different ways Express reads input from the URL. Route params live in the path itself (/users/:id), query strings live after ? (/users?page=2). They end up in different objects and serve different purposes.
Theory
TL;DR
- Route params are part of the path structure:
/users/:idmatches/users/123and puts'123'intoreq.params.id - Query strings come after
?and never affect route matching:/users?page=2still hits the/usershandler - Decision rule: param for "which resource," query string for "how to return it"
- Both values arrive as strings.
req.query.pageis'2', not2
Quick example
const express = require('express');
const app = express();
// Route param: identifies a specific user
app.get('/users/:id', (req, res) => {
// GET /users/123 → req.params.id = '123'
res.json({ userId: req.params.id });
});
// Query string: filters or paginates a collection
app.get('/users', (req, res) => {
// GET /users?page=2&limit=10
const page = parseInt(req.query.page, 10) || 1;
const limit = parseInt(req.query.limit, 10) || 20;
res.json({ page, limit });
});
app.listen(3000);Route params go into req.params. Query values go into req.query. They never share the same object.
Key difference
Express uses the path-to-regexp library to compile route strings into regex patterns at registration time. The :id segment becomes a named capture group, so every incoming request gets matched against it. Query strings are never part of this matching step. They sit after ? in the URL and Node.js parses them separately into req.query. A request to /users/123?sort=asc matches /users/:id, gives you req.params.id === '123', and also gives you req.query.sort === 'asc', all at once.
When to use
- Single resource by ID:
/users/:id,/posts/:postId - Nested resources:
/api/users/:userId/orders/:orderId - Filtering a collection:
/users?status=active&role=admin - Pagination and sorting:
/posts?page=2&sort=desc&limit=20 - Search input:
/search?q=nodejs
A simple test: if you remove the value and the URL no longer points to anything meaningful, put it in a route param. If removing it just changes what you get back, use a query string.
How Express handles this internally
At startup, path-to-regexp (v6+ in Express 4.18+) compiles /users/:id into a regex roughly equivalent to /users/([^/]+?). On each incoming request, router.match() runs that regex and fills req.params. Query strings skip that logic entirely. Node's built-in querystring module reads everything after ? in req.url and decodes it into the plain object you access as req.query.
Common mistakes
Mistake: reading a route param from req.query
// Route: app.get('/users/:id', ...)
// GET /users/123
const id = req.query.id; // undefined, wrong object
const { id } = req.params; // '123', correctThis trips up beginners more than anything else. In code reviews I see it on almost every junior PR that touches routing. req.params and req.query are two separate objects and they never overlap.
Mistake: not converting query string numbers
// GET /users?page=2
const page = req.query.page; // '2', a string
const offset = (page - 1) * 10; // works by accident via coercion
// Better
const page = parseInt(req.query.page, 10) || 1; // 2, a numberThe coercion might look like it works, but page === 2 returns false and page > 1 returns true. Relying on implicit coercion causes subtle bugs.
Mistake: putting search queries in the path
// Awkward: spaces and special characters cause encoding problems
app.get('/search/:q', (req, res) => { ... });
// Better: query strings handle URL encoding naturally
app.get('/search', (req, res) => {
const q = req.query.q; // 'foo bar' already decoded by Express
});Mistake: ignoring repeated query keys
// GET /posts?tag=js&tag=node
// Express 4.18+: req.query.tag = ['js', 'node']
// Single value: req.query.tag = 'js' (a string)
// Safe normalization
const tags = [].concat(req.query.tag || []);Express returns a string when a key appears once and an array when it appears more than once. Always normalize if your route accepts repeated keys.
Real-world usage
- GitHub:
/repos/:owner/:repofor identity,/repos?sort=stars&type=publicfor filtering - Stripe:
/v1/customers/:idfor a specific customer,?limit=10&starting_after=ch_123for pagination - Twitter:
/2/tweets/:idfor a tweet,?max_results=100&query=nodejsfor search - Any REST API follows the same convention: path params for resource identity, query strings for options
Follow-up questions
Q: What is the difference between req.params, req.query, and req.body?
A: req.params is populated from path segments like :id. req.query is populated from the query string after ?. req.body holds the request payload and only works after you add express.json() or express.urlencoded() middleware.
Q: What happens when a route param and a query string share the same name, like /users/:id?id=999?
A: They stay independent. req.params.id holds the path value and req.query.id holds '999'. Express never merges them.
Q: How does Express decode percent-encoded characters like /users/Joe%20Doe?
A: path-to-regexp decodes them automatically. req.params.name will be 'Joe Doe', not 'Joe%20Doe'.
Q: Can you make a route param optional?
A: :id? works in Express 4.x, but two separate routes (/users/:id and /users) are usually cleaner. Optional params can create unexpected overlaps when other routes are nearby.
Q: Why does the same query key sometimes give a string and sometimes an array?
A: Because Express returns a string when the key appears once and an array when it appears more than once. Use [].concat(req.query.tag) to always get an array regardless of how many values are present.
Examples
Basic: user profile lookup
// GET /api/users/42
app.get('/api/users/:id', (req, res) => {
const userId = parseInt(req.params.id, 10);
// Look up user 42 in the database
res.json({ userId });
});req.params.id is always a string. Parse it to a number before using it in a database query.
Real-world: paginated repository list (GitHub-style)
// GET /api/users/octocat/repos?page=2&per_page=30&sort=updated
app.get('/api/users/:username/repos', (req, res) => {
const { username } = req.params;
const page = parseInt(req.query.page, 10) || 1;
const perPage = parseInt(req.query.per_page, 10) || 30;
const sort = req.query.sort || 'updated';
// username = who, page/perPage/sort = how
res.json({ username, page, perPage, sort });
});username identifies the resource. page, per_page, and sort control how it is returned. Swapping them (putting page in the path, or username in the query) breaks REST conventions and makes the API awkward to consume.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.