This article is published in English.
Identify vs Shape: Choosing Route Params or Query Strings in Express
Learn when a value belongs in an Express route parameter versus a query string, how to read req.params and req.query, and how to handle defaults and types safely.
Take the URL /users/42?sort=name&order=asc. The 42 picks out one particular user; sort=name&order=asc only changes how the response is arranged. Mixing up those two roles, for instance treating an ID as a filter or a filter as an ID, is a classic source of awkward Express routes. After reading this guide you will have a simple test for deciding where a value belongs, and you will know how Express exposes each kind and what surprises to expect from them.
Route parameters identify a resource
A route parameter is a named segment that is part of the path pattern itself. It tells the server which specific resource the request is about.
/users/:id
The colon marks :id as a placeholder. When a request for /users/42 arrives, Express matches the pattern and records 42 as the value of id. The same idea applies to any resource that has an identity:
/users/42 → which user
/products/17 → which product
/orders/1042 → which order
Each of these paths names exactly one thing. Without the segment, the question "which one?" has no answer.
Query strings shape the response
A query string is everything after the ?: a list of key=value pairs joined by &. It does not select a resource. Instead it filters, sorts, paginates or otherwise adjusts what comes back.
/users?sort=name&order=asc
Here sort and order leave the resource (the users collection) unchanged and only affect its presentation. More typical examples:
/products?category=electronics&maxPrice=500
/search?q=laptop&page=2
A one-question test for telling them apart
Ask what happens if you delete the value:
- If the route stops making sense (you cannot fetch "a user" without saying which one), the value is an identifier and belongs in the path.
- If the route still works and simply returns the default, unfiltered result (all users, in the standard order), the value is a modifier and belongs in the query string.
This test also hints at error handling. A missing resource behind a route parameter usually deserves a 404, while a filter that matches nothing should normally return 200 with an empty list. For a broader look at resource-oriented URL design, see REST APIs for beginners.
Reading route parameters with req.params
Parameters are declared with a colon in the route path, and their captured values appear on req.params under the same names:
app.get("/users/:id", (req, res) => {
const userId = req.params.id;
res.send(`Fetching user with ID: ${userId}`);
});
A request for /users/42 sets req.params.id to the string "42".
Several parameters in one path
Nested resources simply declare more placeholders:
app.get("/users/:userId/orders/:orderId", (req, res) => {
const { userId, orderId } = req.params;
res.send(`User ${userId}, Order ${orderId}`);
});
For /users/42/orders/1042, destructuring yields userId equal to "42" and orderId equal to "1042". Parameter names must be unique within a route, and they should describe what they identify; userId and orderId read far better than two anonymous ids.
Reading query strings with req.query
Query values need no declaration in the route. Express parses whatever follows the ? and places it on req.query:
app.get("/users", (req, res) => {
const { sort, order } = req.query;
res.send(`Sorting by ${sort}, order: ${order}`);
});
For /users?sort=name&order=asc, you get req.query.sort as "name" and req.query.order as "asc". The route itself stays /users, so the same handler serves both the plain and the sorted request.
Supplying defaults for optional values
Because clients often leave query values out, handlers usually fall back to sensible defaults:
app.get("/products", (req, res) => {
const sort = req.query.sort || "default";
const page = req.query.page || 1;
res.send(`Sorting: ${sort}, Page: ${page}`);
});
A bare /products request still succeeds, using the fallback values. Be aware of one subtlety: when the client does send page=2, page is the string "2", but when it is omitted, it is the number 1. Mixed types like this cause bugs later (string concatenation instead of addition, for example). Convert explicitly, such as Number(req.query.page) || 1, and validate the result before using it in a database query.
Values are not always single strings
A key repeated in the URL, like ?tag=a&tag=b, arrives as an array rather than a string. Depending on the query parser setting, bracket syntax can also produce nested objects. Express 5 changed the default parser to a simpler one than Express 4 used, so check the docs for your version if you rely on nested query objects. Either way, never assume a query value's type; treat req.query as untrusted input.
Deciding which one a route needs
Path parameters for a specific resource
app.get("/users/:id", ...) // one specific user
app.get("/products/:id", ...) // one specific product
app.get("/orders/:orderId", ...) // one specific order
Every one of these routes refers to a single, concrete item. If the route is meaningless without the value, put it in the path.
Query strings for filtering, sorting and pagination
app.get("/users", ...) // ?role=admin&status=active
app.get("/products", ...) // ?category=electronics&maxPrice=500&sort=price
app.get("/search", ...) // ?q=laptop&page=2
Each of these still makes sense with no query at all: "all users", "all products", or an empty search page. Values that merely narrow or reorder the result are optional modifiers and belong after the ?.
Combining both in one route
Real endpoints frequently use both at once. The parameter picks the owner, the query narrows the related data:
app.get("/users/:id/orders", (req, res) => {
const userId = req.params.id; // which user
const status = req.query.status; // optional filter: only their pending orders, for example
res.send(`Orders for user ${userId}, filtered by status: ${status || "all"}`);
});
A request to /users/42/orders?status=pending reads naturally: 42 says whose orders, and status=pending says which of those orders to include. When status is absent, the handler reports "all", matching the idea that a missing modifier means the unfiltered result.
Common questions
Can one route use both?
Yes, and it is very common. The combined example above is the typical pattern: an identifier for the parent resource plus optional filters for its children.
Are parameters always required and query values always optional?
That is a strong convention, not a hard rule. Express does support optional path segments, and nothing prevents an API from requiring a query value. Still, the practical guideline holds: required identifiers go in the path, optional modifiers go in the query with defaults.
Is req.params.id a number?
No. Everything extracted from a URL is a string, even when it looks numeric. Convert it explicitly, for example with Number(req.params.id), and reject values that come out as NaN before touching the database.
What if an expected query value is missing?
The key is simply absent from req.query, so reading it gives undefined. That is exactly why the fallback defaults shown earlier are standard practice.
Wrapping up
Both kinds of values live in the same URL, but they do different jobs. Route parameters, read from req.params, name the specific thing a request is about. Query strings, read from req.query, tune how the answer is filtered, sorted or paged, and should come with defaults. Treat both as untyped strings from the outside world: convert and validate them before use, and your route design will stay predictable as the API grows.