Skip to main content

Command Palette

Search for a command to run...

URL Parameters vs Query Strings in Express.js

Updated
•8 min read•View as Markdown

You are writing Express routes. GET requests, dynamic URLs, filtering data — all of it feels straightforward until you look at a real API and see two different things happening in the URL.

/users/42
/users?role=admin&city=Chennai

Both are GET requests. Both involve the URL. But they work completely differently, they are accessed differently in Express, and they are used for completely different purposes.

Most tutorials mention both in passing and move on. But if you do not understand the distinction clearly, you will use the wrong one in the wrong place — and your API will feel awkward to work with, both for you and for anyone consuming it.

What are URL Parameters?

A URL parameter is a dynamic segment embedded directly inside the route path. It is part of the URL structure itself.

/users/42
/users/99
/users/7

These are not three different routes. They are one route with a dynamic piece — the user ID — changing each time. In Express, you define that dynamic piece with a colon:

app.get("/users/:id", (req, res) => {
  res.json({ userId: req.params.id });
});

The :id is a placeholder. Whatever value sits in that position in the actual URL gets captured and made available to you through req.params. Visit /users/42 and req.params.id is "42". Visit /users/99 and it is "99".

You can have multiple parameters in a single route:

app.get("/users/:userId/posts/:postId", (req, res) => {
  const { userId, postId } = req.params;

  res.json({
    userId: userId,
    postId: postId,
  });
});

Hit /users/5/posts/18 and you get back { userId: "5", postId: "18" }. Each named segment in the route path becomes a key on req.params.

One detail worth remembering — values from req.params always come back as strings, even if the URL contains a number. So req.params.id from /users/42 is the string "42", not the number 42. If you need to do arithmetic or a database lookup that expects an integer, you need to convert it:

app.get("/users/:id", (req, res) => {
  const userId = parseInt(req.params.id);

  if (isNaN(userId)) {
    return res.status(400).json({ message: "Invalid user ID" });
  }

  // Now userId is a proper number
  res.json({ userId });
});

What are Query Parameters?

Query parameters live after the ? in a URL. They are key-value pairs, separated by &, that travel alongside the route without being part of the route structure itself.

/users?role=admin
/users?city=Chennai&role=admin
/products?category=electronics&sort=price&order=asc

The route is still just /users or /products. The query string is additional information layered on top — instructions to the server about how to filter, sort, or modify the response.

In Express, you access these through req.query. Express parses the query string automatically and gives you a clean object:

app.get("/users", (req, res) => {
  console.log(req.query);
  // Visit /users?role=admin&city=Chennai
  // Logs: { role: 'admin', city: 'Chennai' }

  res.json(req.query);
});

No colon syntax in the route definition, no special configuration. Just req.query and all the key-value pairs are there waiting for you.

A realistic example — filtering users by role and city:

const users = [
  { id: 1, name: "Ram Kumar", role: "admin", city: "Chennai" },
  { id: 2, name: "Shiva", role: "developer", city: "Bangalore" },
  { id: 3, name: "Priya", role: "admin", city: "Bangalore" },
  { id: 4, name: "Karthik", role: "developer", city: "Chennai" },
];

app.get("/users", (req, res) => {
  const { role, city } = req.query;

  let result = users;

  if (role) {
    result = result.filter((u) => u.role === role);
  }

  if (city) {
    result = result.filter((u) => u.city === city);
  }

  res.json(result);
});

Hit /users and you get everyone. Hit /users?role=admin and you get only admins. Hit /users?role=admin&city=Bangalore and you get only admins in Bangalore. The route never changed — the query string shaped the response.

Query Parameters for Pagination

Pagination is one of the most common real-world uses of query strings. You do not want to return ten thousand records in one response — you return a page at a time, and the client tells you which page it wants.

app.get("/products", (req, res) => {
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 10;
  const skip = (page - 1) * limit;

  const paginated = products.slice(skip, skip + limit);

  res.json({
    page,
    limit,
    total: products.length,
    data: paginated,
  });
});

Hit /products?page=2&limit=5 and you get the second page of five results. The query string is doing exactly what it is designed for — modifying how the data comes back without changing what resource you are asking for.

The Difference Between Them

URL parameters and query strings look similar from the outside — they are both in the URL, they both pass information to the server. But they have fundamentally different roles.

URL Parameters Query Parameters
Location in URL Inside the path /users/:id After the ? mark
Accessed via req.params req.query
Purpose Identify a specific resource Filter, sort, or modify results
Required or optional Required — part of the route Optional — route works without them
Example /users/42 /users?role=admin
Good for User ID, post ID, product slug Search, pagination, sorting, filtering

The clearest way to think about it — params identify, query strings modify.

When you want a specific user, you use a param. There is one user with ID 42, and /users/42 is the address of that specific resource. If the ID is missing, the route does not match at all.

When you want to filter or search, you use query strings. /users is a valid route that returns all users. /users?role=admin is the same route returning a filtered subset. The query string is optional — remove it and the route still works.

When to Use Which

Use a URL parameter when the value is required to identify the resource you are asking for. No ID, no resource. The parameter is mandatory by nature.

// These make sense as URL params — the ID is the whole point
app.get("/users/:id", ...)
app.get("/posts/:postId", ...)
app.get("/products/:slug", ...)
app.get("/orders/:orderId/items/:itemId", ...)

Use a query string when the value is optional, or when it is filtering or shaping the response rather than identifying a specific thing.

// These make sense as query strings — the route works without them
app.get("/users", ...)           // /users?role=admin&city=Chennai
app.get("/products", ...)        // /products?category=electronics&sort=price
app.get("/posts", ...)           // /posts?tag=javascript&page=2&limit=5
app.get("/search", ...)          // /search?q=express+routing

A practical test — ask yourself: does the URL still point to a meaningful resource if I remove this value? If removing it breaks the request entirely, it belongs in a param. If removing it just gives you a broader or unfiltered result, it belongs in a query string.

Combining Both in One Route

Real APIs use both together all the time. You identify a resource with a param, then modify what you get back with query strings.

// Get posts by a specific user, with optional filtering and pagination
app.get("/users/:userId/posts", (req, res) => {
  const userId = parseInt(req.params.userId);
  const { tag, page = 1, limit = 10 } = req.query;

  let posts = getAllPostsByUser(userId);

  if (tag) {
    posts = posts.filter((p) => p.tags.includes(tag));
  }

  const start = (page - 1) * limit;
  const paginated = posts.slice(start, start + parseInt(limit));

  res.json({
    userId,
    page: parseInt(page),
    total: posts.length,
    data: paginated,
  });
});

/users/5/posts — all posts by user 5. /users/5/posts?tag=javascript — only javascript-tagged posts by user 5. /users/5/posts?tag=javascript&page=2&limit=5 — page two of that filtered result. The param locks in which user. The query strings shape what comes back.

Why This Distinction Matters ?

Getting this right is not just about your code working — it is about your API making sense to anyone who uses it.

An API where you filter by role using /users/admin instead of /users?role=admin is confusing. Does /users/admin mean the user whose ID is "admin"? Or does it mean all admin users? The URL structure sends the wrong signal.

Conversely, an API where you access a specific user with /users?id=42 instead of /users/42 is equally off. The query string implies something optional and filtering-related. An ID is neither of those things — it is required, and it uniquely identifies one resource.

Good API design means the URL structure itself communicates intent. Params say "I am pointing at a specific thing." Query strings say "I am describing how I want that thing returned." When your URLs follow that logic consistently, they become predictable — and predictable APIs are a pleasure to work with.

1 views