Working with Query Strings

1. Accessing Query Parameters

PropertyDescription
req.queryParsed query object
req.urlIncludes raw query string
new URL(req.url, "http://x").searchParamsNative URLSearchParams

Example: Basic access

// GET /search?q=express&page=2
app.get("/search", (req, res) => {
  const { q, page } = req.query;  // strings
  res.json({ q, page });
});

2. Reading Single Query Value

Parser?a=1&b=2?items[a]=1&items[b]=2
simple (default){a:"1",b:"2"}{"items[a]":"1"} (literal)
extended (qs){a:"1",b:"2"}{items:{a:"1",b:"2"}} (nested)

Example: Switch parser

app.set("query parser", "extended");  // enable nested objects
// Disable parsing entirely (req.query = {})
app.set("query parser", false);

3. Handling Array Query Values

Example: Multi-value query

// GET /tags?tag=js&tag=node&tag=express
app.get("/tags", (req, res) => {
  const tags = [].concat(req.query.tag || []);  // normalize to array
  res.json(tags);  // ["js","node","express"]
});

// Or with extended parser: ?tags[]=js&tags[]=node
app.get("/tags2", (req, res) => res.json(req.query.tags));
FormatResult with extended parser
?a=1&a=2["1","2"]
?a[]=1&a[]=2["1","2"]
?a[0]=1&a[1]=2["1","2"]

4. Setting Default Query Values

Example: Defaults & coercion

app.get("/posts", (req, res) => {
  const page = Math.max(1, Number(req.query.page) || 1);
  const limit = Math.min(100, Number(req.query.limit) || 10);
  const sort = req.query.sort ?? "createdAt";
  res.json({ page, limit, sort });
});

5. Validating Query Parameters

Example: zod schema

import { z } from "zod";

const querySchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  sort: z.enum(["asc", "desc"]).default("desc")
});

app.get("/users", (req, res) => {
  const result = querySchema.safeParse(req.query);
  if (!result.success) return res.status(400).json(result.error.flatten());
  res.json(getUsers(result.data));
});
LibraryNote
zodTS-first, supports coerce
express-validatorquery() chain
joiVerbose schema DSL

6. Converting Query Types

SourceConverter
"42"Number(s) or parseInt(s, 10)
"true"s === "true"
"a,b,c"s.split(",")
"2024-01-01"new Date(s)
JSONJSON.parse(s) (catch errors)

7. Handling Missing Query Parameters

Example: Required vs optional

app.get("/search", (req, res) => {
  if (!req.query.q) return res.status(400).json({ error: "q is required" });
  const limit = req.query.limit ?? 10;  // optional with default
  res.json(search(req.query.q, limit));
});

8. Building Query Strings

Example: URLSearchParams

const params = new URLSearchParams({
  page: 2,
  limit: 20,
  sort: "createdAt"
});
const url = `/api/users?${params.toString()}`;
// /api/users?page=2&limit=20&sort=createdAt

// With arrays
const tags = new URLSearchParams();
["js", "node"].forEach(t => tags.append("tag", t));
MethodUse
set(k, v)Replace value
append(k, v)Add additional value
delete(k)Remove
toString()Encoded query string (no leading ?)

9. Parsing Complex Queries

Example: qs library

import qs from "qs";

app.set("query parser", (str) => qs.parse(str, {
  arrayLimit: 100,
  depth: 5,
  parameterLimit: 1000
}));

// ?filter[status]=active&filter[role][]=admin&filter[role][]=editor
// → req.query.filter = { status: "active", role: ["admin", "editor"] }
Warning: Set arrayLimit, depth, and parameterLimit on qs to prevent prototype pollution & DoS via deeply-nested queries.

10. Implementing Search Queries

app.get("/products", async (req, res) => {
  const { search, minPrice, maxPrice, category, sort = "name", page = 1, limit = 20 } = req.query;
  const filter = {};
  if (search)   filter.name = { $regex: search, $options: "i" };
  if (category) filter.category = category;
  if (minPrice || maxPrice) {
    filter.price = {};
    if (minPrice) filter.price.$gte = +minPrice;
    if (maxPrice) filter.price.$lte = +maxPrice;
  }
  const docs = await Product.find(filter).sort(sort).skip((page-1)*limit).limit(+limit);
  res.json({ data: docs, page: +page, limit: +limit });
});