Route Parameters & Query Strings

beginner express routing params query

Two ways to get dynamic data out of a URL:

  • Route params — part of the path itself: /users/4242 is the user ID
  • Query strings — after the ?: /users?role=admin&limit=10

The distinction matters: params identify a resource, query strings filter or modify a request.

Route parameters — req.params

We mark a dynamic segment with :name. Express captures it into req.params:

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

// GET /users/42 → { "userId": "42" }

Multiple params work the same way:

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

// GET /users/42/posts/9 → { "userId": "42", "postId": "9" }
Route matching
Pattern: /users/:userId/posts/:postId
URL: /users/42/posts/9
req.params
{ userId: "42", postId: "9" }
⚠ values are always strings

Gotcha #1: Params are always strings. req.params.id for /users/42 is "42", not 42. Parse with Number(req.params.id) or parseInt.

Gotcha #2: They’re URL-decoded automatically. /users/john%20doe gives req.params.id === "john doe".

Optional params

In Express 4, append ?:

// Express 4
app.get('/users/:id/posts/:postId?', (req, res) => {
  if (req.params.postId) {
    // single post
  } else {
    // all posts for user
  }
});

Heads up — Express 5 changed this syntax to {} braces because they switched to path-to-regexp v8. If we’re on v5, use /users/:id/posts{/:postId}. For most production apps still on v4, the ? form works.

Wildcards

Use * for “match anything”:

app.get('/files/*', (req, res) => {
  // /files/a/b/c → req.params[0] === 'a/b/c'
  res.send(req.params[0]);
});

Handy for serving file paths or catch-all 404 routes.

Query strings — req.query

The bit after ? in the URL. Express parses it into an object:

app.get('/users', (req, res) => {
  const { role, limit = 10 } = req.query;
  res.json({ role, limit });
});

// GET /users?role=admin&limit=20
// → { "role": "admin", "limit": "20" }

Again — values are strings. limit=20 arrives as "20".

Repeated keys become arrays:

// GET /search?tag=node&tag=express
// req.query.tag === ['node', 'express']

Nested objects work too (qs library):

// GET /users?filter[role]=admin&filter[active]=true
// req.query.filter === { role: 'admin', active: 'true' }

When to use which

Think of it like this:

  • /users/:id — “I want the resource at this ID.” Params.
  • /users?role=admin&sort=name — “Filter/modify the result.” Query.

A common interview gotcha: “Should I use a query string or a path param for filtering?” — query. Path params identify; query params modify.

Validation reminder

req.params and req.query are user input. Validate everything. Use zod, joi, express-validator, or hand-rolled checks:

app.get('/users/:id', (req, res) => {
  const id = Number(req.params.id);
  if (!Number.isInteger(id) || id <= 0) {
    return res.status(400).json({ error: 'Invalid id' });
  }
  // ... safe to query DB now
});

Skipping this is how SQL injection and NoSQL injection happen.