Skip to main content

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/:id matches /users/123 and puts '123' into req.params.id
  • Query strings come after ? and never affect route matching: /users?page=2 still hits the /users handler
  • Decision rule: param for "which resource," query string for "how to return it"
  • Both values arrive as strings. req.query.page is '2', not 2

Quick example

javascript
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

javascript
// Route: app.get('/users/:id', ...) // GET /users/123 const id = req.query.id; // undefined, wrong object const { id } = req.params; // '123', correct

This 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

javascript
// 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 number

The 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

javascript
// 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

javascript
// 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/:repo for identity, /repos?sort=stars&type=public for filtering
  • Stripe: /v1/customers/:id for a specific customer, ?limit=10&starting_after=ch_123 for pagination
  • Twitter: /2/tweets/:id for a tweet, ?max_results=100&query=nodejs for 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

javascript
// 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)

javascript
// 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?