Skip to main content

Command Palette

Search for a command to run...

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

Published
7 min read
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

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:

{
  "message": "Fetching user with ID: 42",
  "userId": 42
}

Request: GET /users/42/posts/7

Response:

{
  "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

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:

{
  "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:

{
  "userId": 42,
  "filters": { "status": "published", "sort": "asc" }
}

A Complete Realistic Example

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

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

{ "id": 1, "authorId": 42, "title": "Intro to Express", "status": "published", "category": "backend" }

Filter published backend posts: GET /posts?status=published&category=backend

{
  "count": 1,
  "results": [
    { "id": 1, "authorId": 42, "title": "Intro to Express", "status": "published", "category": "backend" }
  ]
}

When to Use URL Parameters vs Query Strings

ScenarioUseExample
Identify a specific resourceURL paramGET /users/42
Filter a list of resourcesQuery stringGET /users?role=admin
Paginate resultsQuery stringGET /posts?page=2&limit=20
Sort resultsQuery stringGET /posts?sort=created_at&order=desc
Nested resource lookupURL paramGET /users/42/orders/9
Search by keywordQuery stringGET /products?q=wireless+headphones
Toggle a view modifierQuery stringGET /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:

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

More from this blog

ThitaInfo Blogs

63 posts

Making AI simple, fun, and practical for developers.