URL Parameters vs Query Strings in Express.js
Lets handle URL and Query strings in express

Frontend Developer π» | Fueled by curiosity and Tea β | Always learning and exploring new technologies.
Every Express route you write is dealing with URLs. And URLs carry information in more than one way. Two of the most common URL parameters and query strings look similar on the surface but serve very different purposes.
Anatomy of a URL
Before distinguishing between the two, let's look at a URL in full:
https://api.example.com/users/42/posts?status=published&sort=desc&page=2
Breaking it apart:
https:// β protocol
api.example.com β host
/users/42/posts β path (contains a URL parameter: 42)
? β query string delimiter
status=published β query parameter 1
&sort=desc β query parameter 2
&page=2 β query parameter 3
The path is everything before the ?. The query string is everything after it. URL parameters live in the path. Query parameters live after the ?.
What URL Parameters Are
A URL parameter (also called a route parameter or path parameter) is a named placeholder inside the URL path that captures a dynamic value.
In Express, you mark a parameter with a colon:
app.get('/users/:id', (req, res) => {
console.log(req.params.id); // "42" (always a string)
});
When a request arrives for /users/42, Express extracts 42 from that position in the path and puts it in req.params.id.
You can have multiple parameters:
app.get('/users/:userId/posts/:postId', (req, res) => {
console.log(req.params.userId); // "7"
console.log(req.params.postId); // "103"
});
A request to /users/7/posts/103 populates both. The parameter name in :paramName is the key in req.params.
The defining characteristic of URL parameters: they identify which resource you're talking about. Remove the parameter and the URL refers to a completely different resource β or nothing at all.
/users/42 β user with id 42
/users/99 β user with id 99 (different resource)
/users β the users collection (entirely different resource)
The parameter is structural. It's load-bearing. The URL doesn't make sense without it.
What Query Strings Are
A query string is a set of key-value pairs appended to the URL after a ?. They're optional the URL is still valid and meaningful without them.
In Express, query parameters live in req.query:
app.get('/users', (req, res) => {
console.log(req.query.role); // "admin"
console.log(req.query.sort); // "created_at"
console.log(req.query.order); // "desc"
});
A request to /users?role=admin&sort=created_at&order=desc produces that output. If none of those query params were sent, the route still matches /users req.query would just be an empty object {}.
The defining characteristic of query parameters: they modify or filter a request. Remove a query param and you get more results, different ordering, or a different page but you're still talking about the same resource.
/users β all users, default order
/users?sort=name β all users, sorted by name
/users?role=admin β only admins
/users?role=admin&sort=name&page=2 β second page of admins, sorted by name
The resource (/users) is the same. The query params shape how that resource is returned.
Accessing Params in Express: req.params
req.params is an object containing all named URL parameters defined in your route.
app.get('/posts/:postId/comments/:commentId', (req, res) => {
const { postId, commentId } = req.params;
// Always strings β always parseInt or Number() them
const post = parseInt(postId);
const comment = parseInt(commentId);
res.json({ post, comment });
});
Request: GET /posts/5/comments/88 Response: { "post": 5, "comment": 88 }
The string trap: req.params values are always strings, even if they look like numbers. req.params.id === 42 will always be false because you're comparing "42" (string) to 42 (number). Always convert before comparing or using in arithmetic.
const id = parseInt(req.params.id);
// or
const id = Number(req.params.id);
If the parameter might not be a number (like a username or slug), keep it as a string and use it directly.
Optional parameters: Express supports optional segments with ?:
app.get('/users/:id?', (req, res) => {
if (req.params.id) {
res.json({ user: req.params.id });
} else {
res.json({ users: 'all' });
}
});
A single route handles both /users and /users/42. Useful, but in practice it's usually cleaner to keep these as separate routes. Combining them adds complexity for little gain.
Express allows you to define pre-processing logic for a named parameter using app.param():
app.param('id', (req, res, next, id) => {
const parsed = parseInt(id);
if (isNaN(parsed)) {
return res.status(400).json({ error: 'ID must be a number' });
}
req.params.id = parsed; // replace the string with the number
next();
});
// Now every route using :id gets a pre-validated integer
app.get('/users/:id', (req, res) => {
// req.params.id is already a number here β no parseInt needed
res.json({ id: req.params.id });
});
This runs before any route handler that includes :id. You validate and transform once, and every handler benefits. This is one of Express's more underused features.
Accessing Query Strings in Express: req.query
req.query is an object containing all query parameters. Express parses the query string automatically β no setup required.
app.get('/products', (req, res) => {
const {
category, // string or undefined
minPrice, // string or undefined
maxPrice, // string or undefined
sort = 'name', // default to 'name' if not provided
page = '1', // default to '1'
limit = '20',
} = req.query;
// Parse numeric values
const min = minPrice ? parseFloat(minPrice) : 0;
const max = maxPrice ? parseFloat(maxPrice) : Infinity;
const pg = parseInt(page);
const lim = parseInt(limit);
res.json({ category, min, max, sort, pg, lim });
});
Request: GET /products?category=electronics&minPrice=50&sort=price
Response:
{
"category": "electronics",
"min": 50,
"max": null,
"sort": "price",
"pg": 1,
"lim": 20
}
Always default your query params. A request to /products with no query string should still work β req.query will be {} and destructuring gives you undefined for everything. Default values in destructuring handle this cleanly.
The string trap again: same issue as URL parameters. req.query values are always strings. req.query.page === 1 is always false. Parse numbers explicitly.
Arrays in query strings: Express handles repeated keys as arrays automatically:
GET /products?tag=javascript&tag=nodejs&tag=backend
app.get('/products', (req, res) => {
console.log(req.query.tag); // ['javascript', 'nodejs', 'backend']
});
But if only one value is sent, req.query.tag is a string, not an array. To always get an array:
const tags = [].concat(req.query.tag || []);
// or
const tags = Array.isArray(req.query.tag)
? req.query.tag
: req.query.tag ? [req.query.tag] : [];
This is a common bug in Express APIs code that works fine with multiple tags silently breaks when only one is sent.
Here's a clean mental model:
Use a URL parameter when:
It identifies a specific resource
The URL is meaningless or broken without it
There's only one logical value for that position
Use a query string when:
It modifies how a resource (or collection) is returned
It's optional β the endpoint works without it
Multiple key-value pairs might apply simultaneously
Real-World Scenarios
User profile: /users/:id
The id is a URL parameter β it identifies the user. Without it, the URL refers to the collection of all users, not this specific person.
User search: /users?q=alice&role=admin
The q and role are query strings β they filter the collection. Without them, you still get users (just all of them).
Blog post: /posts/:slug
The slug is a URL parameter β it identifies this specific article. /posts is the list; /posts/building-rest-apis-with-express is the specific post.
Blog post list: /posts?tag=nodejs&author=john&page=2
All query strings β they modify the list, not the resource identity.
E-commerce product: /products/:productId
URL parameter β identifies the product.
Product reviews: /products/:productId/reviews?sort=rating&page=1
Mixed. :productId is a parameter β which product's reviews. sort and page are query strings β how to return those reviews.
Iβm currently deep-diving into the JavaScript, building projects and exploring the internals of the web. If you're on a similar journey or just love talking about JavaScript, letβs stay in touch!
Connect on LinkedIn: Satpalsinh's Profile
Follow my Blog: blogs.satpal.cloud
Keep coding and keep building.






