Skip to main content

Command Palette

Search for a command to run...

URL Parameters vs Query Strings in Express.js

Lets handle URL and Query strings in express

Published
β€’7 min read
URL Parameters vs Query Strings in Express.js
S

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!

Keep coding and keep building.