Skip to content
Express.js ex routing 4 min read

Query Strings

Everything after the ? in a URL is the query string — a set of key=value pairs that carry optional, non-hierarchical data like filters, sort order, and page numbers. Unlike route parameters, query strings are not part of the path that Express matches; they travel alongside it and are parsed for you into the req.query object. This page covers reading single values, arrays, and nested objects, choosing a query parser, and the practical patterns of filtering, pagination, and search.

Reading query parameters with req.query

Express parses the query string on every request and exposes the result as req.query. Each key becomes a property whose value is a string (or an array/object for richer inputs — more on that below). You never touch the raw URL; just read the keys you expect.

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

app.get("/search", (req, res) => {
  // GET /search?term=express&limit=10
  const { term, limit } = req.query;
  res.json({ term, limit });
});

app.listen(3000);

Output:

GET /search?term=express&limit=10
{"term":"express","limit":"10"}

Note that limit arrives as the string "10", not the number 10. Query values are always strings (or arrays of strings), so coerce them yourself before using them in arithmetic or comparisons. A missing key is simply undefined, which makes default values easy:

app.get("/items", (req, res) => {
  const page = Number(req.query.page) || 1;
  const sort = req.query.sort || "createdAt";
  res.json({ page, sort });
});

Tip: req.query is read-only conceptually — never trust its contents. Treat every value as untrusted user input and validate or sanitize it before passing it to a database query or the file system.

Arrays and nested queries

A query string can express more than flat strings. When a key repeats, or uses bracket notation, the default parser builds arrays and nested objects automatically.

app.get("/products", (req, res) => {
  // GET /products?tag=node&tag=express&filter[color]=red&filter[size]=large
  res.json(req.query);
});

Output:

{
  "tag": ["node", "express"],
  "filter": { "color": "red", "size": "large" }
}

Repeating tag twice yields an array; filter[color] and filter[size] collapse into a nested filter object. This is powerful, but it also means a key you expect to be a string can arrive as an array if a client sends it twice. Guard against that:

app.get("/products", (req, res) => {
  const raw = req.query.tag;
  const tags = Array.isArray(raw) ? raw : raw ? [raw] : [];
  res.json({ tags });
});

The query parser setting

How req.query is built depends on the query parser application setting. Express 4 defaults to the qs library (the extended parser), which is what produces the nested objects and arrays above. You can change this with app.set.

ValueParserSupports nesting/arraysNotes
"extended"qsYesDefault in Express 4; rich bracket syntax
"simple"Node’s querystringFlat arrays only, no nestingLighter, fewer surprises
falsedisabledNo — req.query is emptyOpt out entirely
functioncustomUp to youReceives the raw string, returns an object
// Use Node's built-in querystring instead of qs
app.set("query parser", "simple");

// Or supply your own parser
app.set("query parser", (str) => new URLSearchParams(str));

Express 5 change: Express 5 sets the default query parser to "simple" rather than "extended". If you rely on nested filter[color] syntax, set app.set("query parser", "extended") explicitly after upgrading, or your nested keys will no longer be parsed into objects.

Query strings shine for optional, combinable parameters. A single list endpoint can support searching, filtering, sorting, and paging — all driven by the query string and all optional. Here is a realistic handler that reads each parameter defensively and applies sensible bounds.

const router = express.Router();

router.get("/articles", async (req, res) => {
  const { q, category, sort = "newest" } = req.query;

  // Pagination with clamped, validated numbers
  const page = Math.max(1, Number(req.query.page) || 1);
  const limit = Math.min(100, Math.max(1, Number(req.query.limit) || 20));
  const offset = (page - 1) * limit;

  const where = {};
  if (category) where.category = category;
  if (q) where.title = { contains: q };

  const order = sort === "oldest" ? "asc" : "desc";

  const articles = await db.articles.find({ where, order, offset, limit });
  const total = await db.articles.count({ where });

  res.json({
    data: articles,
    page,
    limit,
    total,
    pages: Math.ceil(total / limit),
  });
});

Output:

GET /articles?q=express&category=tutorials&page=2&limit=10
{
  "data": [ ... ],
  "page": 2,
  "limit": 10,
  "total": 47,
  "pages": 5
}

Clamping limit to a maximum (100 here) prevents a client from requesting an unbounded result set, and forcing page to be at least 1 avoids negative offsets. This defensive parsing is the difference between a robust API and one that crashes on ?page=-5&limit=999999.

Best Practices

  • Always coerce query values — they are strings. Use Number(), === "true", or a validation library before relying on them.
  • Provide defaults with || or destructuring defaults so missing parameters degrade gracefully instead of throwing.
  • Clamp numeric inputs like limit and page to safe minimum and maximum bounds to protect your database.
  • Handle the array case: a key sent twice becomes an array, so normalize with Array.isArray() when you expect a single value.
  • Be explicit about the query parser setting in Express 5 if your client uses nested key[sub] syntax.
  • Validate and whitelist sort and filter fields against an allow-list rather than passing user strings straight into a query.
  • Keep required, hierarchical identifiers in the path (route parameters) and use query strings only for optional, orthogonal options.
Last updated June 14, 2026
Was this helpful?