URL Parameters and Query Strings: The Two Ways Your URL Talks to Your Server

I spent an embarrassing amount of time early on not understanding why sometimes data showed up in my URL after a slash and sometimes after a question mark. I would look at URLs like /users/42 and /users?name=saumya and think they were doing the same thing. They are not. They solve different problems, and once the distinction clicked, half of my routing confusion disappeared overnight.
This post covers what URL parameters and query strings are, how they differ, and how you actually use them in Express. If you have been copy-pasting route code without fully understanding the :id part or the req.query part, this should clear that up.
A URL has parts, and they have names
Before anything else, look at a URL and know what each piece is called. Take this one:
https://api.foodapp.com/restaurants/5f2b/menu?category=biryani&sort=price
The path is /restaurants/5f2b/menu. The 5f2b in there is a URL parameter, a variable piece embedded in the path itself. Everything after the ? is the query string. category=biryani and sort=price are query parameters, key-value pairs separated by &.
Two different mechanisms. Same URL. Different jobs.
URL parameters: the "which one" part
URL parameters identify a specific resource. When you write /users/42, you are saying "I want user number 42." Not a list of users. Not a filtered set. One specific user.
In Express, you define a URL parameter by putting a colon before the name in your route:
// :id is the parameter
app.get('/users/:id', (req, res) => {
console.log(req.params);
// { id: '42' }
console.log(req.params.id);
// '42'
});
When someone hits GET /users/42, Express matches the route and puts 42 into req.params.id. The name after the colon is what you use to access it. Call it :id, :userId, :slug, whatever makes sense for your route.
You can have multiple parameters in one route. A food delivery app might need both a restaurant and a menu item:
app.get('/restaurants/:restaurantId/menu/:itemId', (req, res) => {
console.log(req.params);
// { restaurantId: '5f2b', itemId: 'a1c3' }
const restaurant = await Restaurant.findById(req.params.restaurantId);
const item = restaurant.menu.id(req.params.itemId);
res.json(item);
});
The URL /restaurants/5f2b/menu/a1c3 fills both params. Each :name in the route definition maps to the matching segment in the actual URL, left to right.
One thing to watch: req.params.id is always a string. Even if the URL is /users/42, you get the string '42', not the number 42. If you are passing it to a database query that expects a number, you need to convert it yourself. MongoDB ObjectIds work fine as strings, but if your IDs are numeric you will want parseInt(req.params.id) or Number(req.params.id).
Query strings: the "how do you want it" part
Query strings modify a request. They filter, sort, paginate, or adjust what comes back, but they do not change which resource you are looking at. The URL /users means "all users." The URL /users?role=rider&sort=name still means "all users," just filtered to riders and sorted by name.
Express parses query strings automatically. Everything after the ? ends up in req.query:
// URL: /users?role=rider&sort=name&page=2
app.get('/users', (req, res) => {
console.log(req.query);
// { role: 'rider', sort: 'name', page: '2' }
console.log(req.query.role);
// 'rider'
console.log(req.query.page);
// '2' (still a string)
});
Same as params, everything in req.query is a string. page comes back as '2', not 2. Parse it before doing math with it.
Query parameters are optional by nature. If someone hits /users without any query string, req.query is just an empty object {}. Your code needs to handle that. Default values are a good habit:
app.get('/users', async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const sort = req.query.sort || 'createdAt';
const role = req.query.role; // undefined if not provided
const filter = {};
if (role) filter.role = role;
const users = await User.find(filter)
.sort(sort)
.skip((page - 1) * limit)
.limit(limit);
res.json(users);
});
This route works whether you call it as /users, /users?page=3, or /users?role=admin&sort=name&page=2&limit=5. The defaults cover whatever the caller did not specify.
The difference, shown side by side
The short version: params say "which thing," query strings say "how you want it."
If you are fetching one specific user, that is a param: /users/42. If you are searching for users who match some criteria, those criteria go in the query string: /users?city=mumbai&role=rider. If you are getting a specific order for a specific user, both user and order are params: /users/42/orders/99. If you want that order's invoice in PDF format instead of JSON, that is a query: /users/42/orders/99?format=pdf.
I used to overthink this. The test I use now is simple: would the route still make sense without this piece of data? If removing it makes the URL meaningless (like /users/ with no ID when you want one user), it is a param. If removing it just gives you a broader or default response, it is a query.
Using both at the same time
Most real routes use params and query strings together. Here is a route that fetches orders for a specific user, with optional filtering:
// GET /users/:userId/orders?status=delivered&page=1
app.get('/users/:userId/orders', async (req, res) => {
const { userId } = req.params;
const { status, page = 1, limit = 10 } = req.query;
const filter = { user: userId };
if (status) filter.status = status;
const orders = await Order.find(filter)
.sort({ createdAt: -1 })
.skip((parseInt(page) - 1) * parseInt(limit))
.limit(parseInt(limit))
.populate('restaurant', 'name');
res.json(orders);
});
The param :userId tells us whose orders we are looking at. The query strings status, page, and limit let the caller control the response shape. Remove the query strings and you still get a valid response: all orders for that user, first page, default limit. Remove the param and the route does not match at all.
A few things that tripped me up
Params are positional. Express matches them left to right in the URL path. If you have two routes like app.get('/users/:id') and app.get('/users/active'), the order you define them matters. Express checks routes in the order they are registered. If :id comes first, a request to /users/active matches it with req.params.id equal to 'active'. Put the specific route before the parameterized one:
// specific route first
app.get('/users/active', (req, res) => {
// handles /users/active
});
// parameterized route second
app.get('/users/:id', (req, res) => {
// handles /users/42, /users/abc, etc.
});
Query strings do not affect route matching. Express matches routes based on the path only. /users and /users?role=admin hit the same route handler. You never define query parameters in your route string.
Empty query values are valid. A URL like /users?name= gives you req.query.name as an empty string '', which is truthy in a conditional check... actually no, empty strings are falsy in JavaScript. So if (req.query.name) fails on both undefined and ''. If you need to distinguish between "not provided" and "provided but empty," check for undefined explicitly:
if (req.query.name !== undefined) {
// they sent the parameter, even if it is empty
}
When to use which
Params for identification: /products/:productId, /orders/:orderId, /users/:userId/profile.
Query strings for everything optional: search terms, filters, sort order, pagination, format preferences, feature flags.
If you catch yourself putting optional data in the path, move it to the query string. If you catch yourself using query strings to identify a specific resource, move it to a param. The URL should read like a sentence. /restaurants/5f2b/menu reads as "the menu for restaurant 5f2b." That makes sense. /restaurants?id=5f2b&show=menu technically works but reads like a database query leaked into your URL.
REST API conventions reinforce this pattern. You will see it everywhere once you start reading API documentation for services like Stripe, GitHub, or Twilio. The resource goes in the path. The options go after the question mark.





