# URL Parameters vs Query Strings in Express.js

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:

```shell
https://api.example.com/users/42/posts?status=published&sort=desc&page=2
```

Breaking it apart:

```plaintext
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:

```javascript
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`](http://req.params.id).

You can have multiple parameters:

```javascript
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.

```plaintext
/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`:

```javascript
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.

```plaintext
/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.

![](https://cdn.hashnode.com/uploads/covers/6950d1ca85602739c40abd67/cdf14672-c324-43ed-a970-b666832a2b23.png align="center")

## Accessing Params in Express: `req.params`

`req.params` is an object containing all named URL parameters defined in your route.

```javascript
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`](http://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.

```javascript
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 `?`:

```javascript
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()`:

```javascript
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.

```javascript
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:

```json
{
  "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`](http://req.query.page) `=== 1` is always `false`. Parse numbers explicitly.

**Arrays in query strings:** Express handles repeated keys as arrays automatically:

```plaintext
GET /products?tag=javascript&tag=nodejs&tag=backend
```

```javascript
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:

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

![](https://cdn.hashnode.com/uploads/covers/6950d1ca85602739c40abd67/54db9685-650d-4054-b57a-b46b33609c5d.png align="center")

## 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**](https://www.linkedin.com/in/satpalsinhrana)
    
*   **Follow my Blog:** [**blogs.satpal.cloud**](http://blogs.satpal.cloud)
    

**Keep coding and keep building.**
