# URL Parameters vs Query Strings in Express.js: When to Use Each and Why

# URL Parameters vs Query Strings in Express.js: When to Use Each and Why

**TL;DR:** URL parameters identify a specific resource (e.g., `/users/42`). Query strings filter, sort, or modify how that resource is returned (e.g., `/users?role=admin&sort=asc`). Mixing them up leads to poorly designed APIs that are hard to maintain.

> This post assumes familiarity with Node.js and basic Express.js routing. No prior API design experience required.

---

## Problem

When building a REST API in Express, you'll frequently need to pass data through the URL. Two mechanisms exist for this: URL parameters and query strings. Most tutorials show you the syntax but skip the *why* — which means developers end up with routes like `/search/:keyword` when they should use `/search?q=keyword`, or use `/users?id=42` when `/users/42` is the right call.

Getting this wrong produces APIs that are hard to document, hard to cache, and confusing to consume.

---

## Solution

### Understanding the URL Structure

Before touching Express, understand what a URL is made of:

```
https://api.example.com/users/42/posts?status=published&sort=desc
         |_____________||______||____|  |_________________________________|
              host        path  param         query string
```

- **Path segments** define the resource hierarchy
- **URL parameters** (`:id`) are embedded in the path — they *are* part of the resource address
- **Query strings** come after `?` and are key-value pairs separated by `&` — they *modify* the request

---

### What Are URL Parameters?

URL parameters are named placeholders in your route path. They are part of the URL path itself and are used to **identify a specific resource**.

```
GET /users/42
GET /users/42/posts/7
GET /products/iphone-15-pro
```

Think of them as the *noun* — they tell your server *which thing* you're talking about.

#### Accessing URL Parameters in Express

```javascript
const express = require('express');
const app = express();

// Route with a URL parameter
app.get('/users/:userId', (req, res) => {
  const userId = req.params.userId; // '42' (always a string)
  
  // In a real app, you'd query a database here
  res.json({
    message: `Fetching user with ID: ${userId}`,
    userId: parseInt(userId, 10)
  });
});

// Multiple URL parameters
app.get('/users/:userId/posts/:postId', (req, res) => {
  const { userId, postId } = req.params;
  
  res.json({
    message: `Fetching post ${postId} by user ${userId}`,
    userId: parseInt(userId, 10),
    postId: parseInt(postId, 10)
  });
});

app.listen(3000, () => console.log('Server running on port 3000'));
```

**Request:** `GET /users/42`

**Response:**
```json
{
  "message": "Fetching user with ID: 42",
  "userId": 42
}
```

**Request:** `GET /users/42/posts/7`

**Response:**
```json
{
  "message": "Fetching post 7 by user 42",
  "userId": 42,
  "postId": 7
}
```

> **Note:** `req.params` values are always strings. Parse them explicitly if you need numbers.

---

### What Are Query Strings?

Query strings are key-value pairs appended to a URL after `?`. They are used to **filter, sort, paginate, or modify** how a resource is returned. They do not change *which resource* you're targeting — they change *how* you want it.

```
GET /users?role=admin
GET /posts?status=published&sort=desc&page=2&limit=10
GET /products?category=electronics&minPrice=100&maxPrice=500
```

Think of them as *adjectives* — they describe how you want the resource.

#### Accessing Query Strings in Express

```javascript
const express = require('express');
const app = express();

// Route with query string parameters
app.get('/users', (req, res) => {
  const {
    role,
    sort = 'asc',   // default value
    page = 1,
    limit = 10
  } = req.query;

  // Build a response showing what was received
  const filters = {};
  if (role) filters.role = role;
  
  res.json({
    message: 'Fetching users with filters',
    filters,
    pagination: {
      page: parseInt(page, 10),
      limit: parseInt(limit, 10)
    },
    sort
  });
});

// Combining URL params AND query strings
app.get('/users/:userId/posts', (req, res) => {
  const { userId } = req.params;           // identifies WHICH user
  const { status, sort = 'desc' } = req.query;  // filters HOW posts are returned

  res.json({
    userId: parseInt(userId, 10),
    filters: { status, sort }
  });
});

app.listen(3000, () => console.log('Server running on port 3000'));
```

**Request:** `GET /users?role=admin&sort=desc&page=2&limit=5`

**Response:**
```json
{
  "message": "Fetching users with filters",
  "filters": { "role": "admin" },
  "pagination": { "page": 2, "limit": 5 },
  "sort": "desc"
}
```

**Request:** `GET /users/42/posts?status=published&sort=asc`

**Response:**
```json
{
  "userId": 42,
  "filters": { "status": "published", "sort": "asc" }
}
```

---

### A Complete Realistic Example

Here's a more complete example modeling a blog API:

```javascript
const express = require('express');
const app = express();

// Mock data
const posts = [
  { id: 1, authorId: 42, title: 'Intro to Express', status: 'published', category: 'backend' },
  { id: 2, authorId: 42, title: 'Node.js Streams', status: 'draft', category: 'backend' },
  { id: 3, authorId: 42, title: 'CSS Grid Guide', status: 'published', category: 'frontend' },
  { id: 4, authorId: 99, title: 'React Hooks', status: 'published', category: 'frontend' },
];

// Get a specific post by ID — URL param identifies the resource
app.get('/posts/:postId', (req, res) => {
  const postId = parseInt(req.params.postId, 10);
  const post = posts.find(p => p.id === postId);

  if (!post) {
    return res.status(404).json({ error: `Post with ID ${postId} not found` });
  }

  res.json(post);
});

// List posts with optional filters — query strings modify the result set
app.get('/posts', (req, res) => {
  const { status, category, authorId } = req.query;

  let results = [...posts];

  if (status) {
    results = results.filter(p => p.status === status);
  }
  if (category) {
    results = results.filter(p => p.category === category);
  }
  if (authorId) {
    results = results.filter(p => p.authorId === parseInt(authorId, 10));
  }

  res.json({
    count: results.length,
    results
  });
});

app.listen(3000, () => console.log('API running on http://localhost:3000'));
```

**Fetch a specific post:** `GET /posts/1`
```json
{ "id": 1, "authorId": 42, "title": "Intro to Express", "status": "published", "category": "backend" }
```

**Filter published backend posts:** `GET /posts?status=published&category=backend`
```json
{
  "count": 1,
  "results": [
    { "id": 1, "authorId": 42, "title": "Intro to Express", "status": "published", "category": "backend" }
  ]
}
```

---

### When to Use URL Parameters vs Query Strings

| Scenario | Use | Example |
|---|---|---|
| Identify a specific resource | URL param | `GET /users/42` |
| Filter a list of resources | Query string | `GET /users?role=admin` |
| Paginate results | Query string | `GET /posts?page=2&limit=20` |
| Sort results | Query string | `GET /posts?sort=created_at&order=desc` |
| Nested resource lookup | URL param | `GET /users/42/orders/9` |
| Search by keyword | Query string | `GET /products?q=wireless+headphones` |
| Toggle a view modifier | Query string | `GET /reports?format=csv` |

**Decision rule:**
- If removing it makes the URL point to a *different thing* → URL parameter
- If removing it returns *more results* or changes the format → query string

---

## Trade-offs

**URL Parameters**
- ✅ Clean, semantic URLs that are easy to read and cache
- ✅ Clearly communicates resource hierarchy
- ❌ Route conflicts possible if not ordered carefully (e.g., `/users/me` vs `/users/:id` — Express matches top-down)
- ❌ Not suitable for optional data — every param in the path is required

**Query Strings**
- ✅ Naturally optional — omitting a query param doesn't break the route
- ✅ Flexible for multiple filters without changing route structure
- ❌ Can get unwieldy with many parameters
- ❌ Sensitive data should never go in query strings — they appear in server logs and browser history

**Route ordering gotcha with URL params:**

```javascript
// Define specific routes BEFORE parameterized ones
app.get('/users/me', (req, res) => { /* won't be shadowed */ });
app.get('/users/:userId', (req, res) => { /* catches everything else */ });

// If reversed, GET /users/me would match :userId = 'me'
```

---

## Results

Applying these rules consistently produces APIs where:
- Routes are self-documenting — the URL tells you exactly what resource you're addressing
- Filtering logic stays cleanly separated from resource identification
- Caching works correctly — `/users/42` can be cached at the CDN layer; `/users?role=admin` typically should not
- Client developers can predict URL structure without reading docs

---

## Conclusion

URL parameters and query strings solve different problems. Parameters are identifiers — they point to a thing. Query strings are modifiers — they describe how you want that thing. Once that mental model is clear, the right choice in any situation becomes obvious. Start by asking: *am I identifying a resource, or am I filtering/modifying one?*

**Next step:** Apply this to your next Express route. Audit an existing API you've built — are there query strings being used where URL params belong, or vice versa?

---

## Further Reading

- [Express.js Routing Documentation](https://expressjs.com/en/guide/routing.html)
- [MDN — Identifying resources on the Web](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Identifying_resources_on_the_Web)
- [REST API Design: URL Parameters vs Query Parameters](https://restfulapi.net/resource-naming/)
- [HTTP Caching — MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching)
- [Express req.params vs req.query — Official Docs](https://expressjs.com/en/4x/api.html#req.params)
