Skip to main content

Command Palette

Search for a command to run...

URL Parameters vs Query Strings in Express.js

They look similar in a URL, but they solve completely different problems. Mastering this distinction makes your APIs clean and predictable.

Updated
10 min read
URL Parameters vs Query Strings in Express.js
A

Hi, I’m Abdul Samad. A web development learner and tech enthusiast. I write about what I learn, share practical coding tips, and publish in-depth blogs on programming and modern web development.

Check out my full collection of blogs on Hashnode: https://abdulsamad30.hashnode.dev/

Connect with me on X for quick updates and insights: @abdul_sama60108

If you've spent any time working with web APIs, you've definitely seen URLs like these:

/users/42
/products?category=shoes&sort=price

Both of these are ways to send information to a server through a URL. But they serve different purposes, behave differently, and should be used in different situations. Mixing them up is a very common beginner mistake and it's one that leads to APIs that feel inconsistent and hard to use.

In this post, we're going to break down URL parameters and query strings clearly, show you how to access both in Express.js, and give you a practical sense of when to use which one.


What URL Parameters Are

URL parameters often called route parameters or just params are dynamic parts of a URL path. They act as identifiers for a specific resource. When you want to point at one particular thing, a URL parameter is how you do it.

Think of a URL parameter like a house address. If someone asks you to deliver a package to "42 Maple Street," the number 42 uniquely identifies that specific house on that street. It's not a filter, it's not optional it's a direct pointer to one specific place.

In Express, URL parameters are defined with a colon (:) in front of them in the route definition:

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

const users = [
  { id: 1, name: 'Alice', email: 'alice@example.com' },
  { id: 2, name: 'Bob', email: 'bob@example.com' },
  { id: 3, name: 'Charlie', email: 'charlie@example.com' }
];

// :id is a URL parameter
app.get('/users/:id', function (req, res) {
  const userId = parseInt(req.params.id);
  const user = users.find(u => u.id === userId);

  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }

  res.json(user);
});

app.listen(3000, function () {
  console.log('Server running on port 3000');
});

Expected Output (GET /users/2):

{ "id": 2, "name": "Bob", "email": "bob@example.com" }

Expected Output (GET /users/99):

{ "error": "User not found" }

The :id in the route definition is a placeholder. When a real request comes in with /users/2, Express captures 2 and makes it available as req.params.id. The parameter is part of the URL structure itself not something tacked on at the end.


What Query Parameters Are

Query parameters also called query strings are key-value pairs that appear at the end of a URL after a ? symbol. Multiple query parameters are separated by &.

If URL parameters are identifiers, query parameters are modifiers. They don't point to a specific resource they describe how you want that resource (or collection of resources) filtered, sorted, limited, or formatted.

/products?category=shoes&sort=price&limit=10

Here, you're not asking for one specific product. You're asking for a collection of products, filtered by category, sorted by price, with a limit of 10 results. Those are modifications to the response not identifiers.

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

const products = [
  { id: 1, name: 'Running Shoes', category: 'shoes', price: 80 },
  { id: 2, name: 'Sneakers', category: 'shoes', price: 50 },
  { id: 3, name: 'Jacket', category: 'clothing', price: 120 },
  { id: 4, name: 'T-Shirt', category: 'clothing', price: 25 },
  { id: 5, name: 'Boots', category: 'shoes', price: 110 }
];

app.get('/products', function (req, res) {
  let results = [...products];

  // Filter by category if provided
  if (req.query.category) {
    results = results.filter(p => p.category === req.query.category);
  }

  // Sort by price if requested
  if (req.query.sort === 'price') {
    results.sort((a, b) => a.price - b.price);
  }

  // Limit results if specified
  if (req.query.limit) {
    results = results.slice(0, parseInt(req.query.limit));
  }

  res.json(results);
});

app.listen(3000, function () {
  console.log('Server running on port 3000');
});

Expected Output (GET /products?category=shoes&sort=price):

[
  { "id": 2, "name": "Sneakers", "category": "shoes", "price": 50 },
  { "id": 1, "name": "Running Shoes", "category": "shoes", "price": 80 },
  { "id": 5, "name": "Boots", "category": "shoes", "price": 110 }
]

Expected Output (GET /products?category=clothing&limit=1):

[
  { "id": 3, "name": "Jacket", "category": "clothing", "price": 120 }
]

Notice that req.query gives you all the query parameters as an object. You don't need to parse the URL yourself Express does that automatically.


The Difference Between Them

Let's look at a complete URL to understand exactly how these two things relate to each other structurally:

<https://api.example.com/users/42/posts?status=published&sort=date>

The structural differences are clear:

  • URL parameters are embedded in the path they're part of the URL's directory-like structure

  • Query strings come after the ? they're separate from the path itself

  • URL parameters are typically required for the route to match /users/ without an ID wouldn't match /users/:id

  • Query strings are usually optional /products works fine without any filters

In Express, they're also accessed differently:

Feature URL Parameters Query Strings
Access via req.params req.query
URL position Inside the path After the ?
Syntax in URL /users/42 ?sort=price
Typically Required Optional

Accessing Params in Express

You've already seen req.params in action, but let's look at a slightly more complete example with multiple parameters in a single route. Express lets you define as many named parameters as you need.

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

const posts = [
  { id: 1, userId: 1, title: 'Hello World', status: 'published' },
  { id: 2, userId: 1, title: 'My Second Post', status: 'draft' },
  { id: 3, userId: 2, title: 'Another Post', status: 'published' }
];

// Multiple URL parameters in one route
app.get('/users/:userId/posts/:postId', function (req, res) {
  const userId = parseInt(req.params.userId);
  const postId = parseInt(req.params.postId);

  console.log('Params received:', req.params);

  const post = posts.find(p => p.userId === userId && p.id === postId);

  if (!post) {
    return res.status(404).json({ error: 'Post not found for this user' });
  }

  res.json(post);
});

app.listen(3000, function () {
  console.log('Server running on port 3000');
});

Expected Output (GET /users/1/posts/2):

Terminal:

Params received: { userId: '1', postId: '2' }

Response:

{ "id": 2, "userId": 1, "title": "My Second Post", "status": "draft" }

Expected Output (GET /users/1/posts/99):

{ "error": "Post not found for this user" }

One important thing to note: req.params values are always strings. Even though userId looks like a number in the URL, Express gives you the string '1', not the number 1. That's why parseInt() is used to convert it before comparing against numeric IDs.


Accessing Query Strings in Express

Query strings are accessed through req.query, which Express automatically parses into an object. Each key in req.query corresponds to a query parameter in the URL.

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

const articles = [
  { id: 1, title: 'Node.js Basics', author: 'Alice', tag: 'nodejs', published: true },
  { id: 2, title: 'Express Guide', author: 'Bob', tag: 'express', published: true },
  { id: 3, title: 'Draft Post', author: 'Alice', tag: 'nodejs', published: false },
  { id: 4, title: 'REST APIs', author: 'Charlie', tag: 'api', published: true }
];

app.get('/articles', function (req, res) {
  console.log('Query string received:', req.query);

  let results = [...articles];

  if (req.query.author) {
    results = results.filter(a => a.author === req.query.author);
  }

  if (req.query.tag) {
    results = results.filter(a => a.tag === req.query.tag);
  }

  if (req.query.published !== undefined) {
    const isPublished = req.query.published === 'true';
    results = results.filter(a => a.published === isPublished);
  }

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

app.listen(3000, function () {
  console.log('Server running on port 3000');
});

Expected Output (GET /articles?author=Alice&published=true):

Terminal:

Query string received: { author: 'Alice', published: 'true' }

Response:

{
  "count": 1,
  "results": [
    { "id": 1, "title": "Node.js Basics", "author": "Alice", "tag": "nodejs", "published": true }
  ]
}

Expected Output (GET /articles?tag=nodejs):

{
  "count": 2,
  "results": [
    { "id": 1, "title": "Node.js Basics", "author": "Alice", "tag": "nodejs", "published": true },
    { "id": 3, "title": "Draft Post", "author": "Alice", "tag": "nodejs", "published": false }
  ]
}

Like params, query string values are also strings even if they look like numbers or booleans. Notice how req.query.published comes in as the string 'true', not the boolean true. Always handle that conversion explicitly in your code.


When to Use Params vs Query

This is the practical question that matters most. Here's a clear mental model:

Use URL parameters when:

  • You're identifying a specific, unique resource

  • The value is required for the route to make sense

  • You're navigating a resource hierarchy

Use query strings when:

  • You're filtering, sorting, or paginating a collection

  • The values are optional and have sensible defaults

  • You're modifying how a response is returned, not which resource you're accessing

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

const users = [
  { id: 1, name: 'Alice', role: 'admin', active: true },
  { id: 2, name: 'Bob', role: 'user', active: true },
  { id: 3, name: 'Charlie', role: 'user', active: false }
];

// URL Parameter: Fetch one specific user by ID
// Query would make no sense here  you KNOW which user you want
app.get('/users/:id', function (req, res) {
  const user = users.find(u => u.id === parseInt(req.params.id));

  if (!user) return res.status(404).json({ error: 'User not found' });
  res.json(user);
});

// Query String: Filter the user collection
// URL param would make no sense here  you're not fetching ONE thing
app.get('/users', function (req, res) {
  let results = [...users];

  if (req.query.role) {
    results = results.filter(u => u.role === req.query.role);
  }

  if (req.query.active !== undefined) {
    results = results.filter(u => u.active === (req.query.active === 'true'));
  }

  res.json(results);
});

app.listen(3000, function () {
  console.log('Server running on port 3000');
});

Expected Output (GET /users/1 fetching a specific user):

{ "id": 1, "name": "Alice", "role": "admin", "active": true }

Expected Output (GET /users?role=user&active=true filtering a collection):

[
  { "id": 2, "name": "Bob", "role": "user", "active": true }
]

Wrapping Up

URL parameters and query strings are both ways to pass information through a URL but they're not interchangeable. Each has a clear, specific purpose:

  • URL parameters (req.params) identify a specific resource. They're embedded in the route path, required for the route to match, and used when you know exactly which item you want.

  • Query strings (req.query) modify or filter a response. They come after the ?, are typically optional, and are used when you're working with a collection and want to shape the results.

  • Both come back as strings always convert to the appropriate type (parseInt, boolean comparison) before using them in logic.

  • The rule of thumb is simple: params tell you who, query tells you how.

Getting this distinction right from the start makes your APIs cleaner, more predictable, and much easier for other developers (and future you) to work with. It's a small decision that has a big impact on the overall quality of your API design.

FIN ✌️