NodeJSParsing Query Strings

Parsing Query Strings

The query string is the ?key=value&key2=value2 part of a URL — how clients pass parameters to a GET request (filters, pagination, search terms). In modern Node you parse it with the WHATWG URL class and URLSearchParams, the same APIs the browser uses. This page covers reading query parameters correctly, the encoding rules, and the legacy querystring module you'll still see in older code.

Parsing with the URL class

JS
import { createServer } from 'node:http'

createServer((req, res) => {
  // req.url is path + query, so give URL a base to resolve against:
  const url = new URL(req.url, `http://${req.headers.host}`)
  const params = url.searchParams        // a URLSearchParams object

  const q    = params.get('q')           // ?q=node → 'node'
  const page = params.get('page')        // ?page=2 → '2'  (always a STRING)

  res.end(`searching "${q}" page ${page}`)
}).listen(3000)
`req.url` is path-relative, so `new URL` needs a base
`req.url` is just `/search?q=node`, not an absolute URL, so `new URL(req.url)` alone throws. Supply a base built from the `Host` header. Then `url.searchParams` gives you a `URLSearchParams` instance with all the accessors below — and `url.pathname` for [routing](/nodejs/routing-basics).
The URLSearchParams API

Method

Returns

get(name)

First value, or null if absent

getAll(name)

Array of all values for repeated keys

has(name)

Boolean — is the key present?

keys() / values() / entries()

Iterators over the pairs

set(name, value)

Set (replacing existing)

append(name, value)

Add without replacing

toString()

Serialize back to a query string

JS
const params = new URLSearchParams('tag=js&tag=node&page=2')

params.get('tag')        // 'js'           — only the FIRST
params.getAll('tag')     // ['js', 'node'] — all of them
params.has('page')       // true
[...params]              // [['tag','js'], ['tag','node'], ['page','2']]
The gotcha: every value is a string
Query parameters are ALWAYS strings — convert and validate
There are no numbers, booleans, or arrays in a query string — `?page=2` gives you the string `'2'`, and `?active=true` gives `'true'` (a truthy string even if it were `'false'`!). You must convert explicitly and handle missing/garbage input. `Number(params.get('page'))` on a missing key yields `NaN`; on `'abc'` also `NaN`. Always provide defaults and validate ranges.

JS
function parsePagination(params) {
  // Coerce + default + clamp — never trust the raw string:
  const page  = Math.max(1, parseInt(params.get('page'), 10) || 1)
  const limit = Math.min(100, Math.max(1, parseInt(params.get('limit'), 10) || 20))
  const active = params.get('active') === 'true'   // explicit boolean check
  return { page, limit, active }
}
Repeated keys vs the bracket convention

There's no single standard for arrays in query strings. URLSearchParams treats repeated keys as multiple values (?id=1&id=2getAll('id')). Other ecosystems use a bracket convention (?id[]=1&id[]=2 or ?ids=1,2) — but the URL class does not interpret brackets specially:

JS
const p = new URLSearchParams('id=1&id=2&filter[status]=open')

p.getAll('id')             // ['1', '2']            — repeated keys work
p.get('filter[status]')    // 'open'  — the literal key 'filter[status]', no nesting
No automatic nesting — pick a convention explicitly
If you want nested objects (`filter[status]=open` → `{ filter: { status: 'open' } }`) or comma-arrays, you parse that yourself or use a library like `qs`. Decide on a convention and document it; don't assume clients and server agree. For most APIs, repeated keys (`?tag=a&tag=b`) are the simplest interoperable choice.
Encoding and special characters
Build query strings with URLSearchParams — never concatenate raw values
Values containing `&`, `=`, spaces, or unicode must be percent-encoded, or they corrupt the query string (or enable injection). `URLSearchParams` encodes automatically on `toString()`; manual string-building does not. Always construct queries through the API:

JS
// ✗ Broken — the value's & and space break the query
const bad = '/search?q=' + 'tom & jerry'      // ?q=tom & jerry  (invalid)

// ✓ Correct — proper percent-encoding
const params = new URLSearchParams({ q: 'tom & jerry', page: '2' })
const good = '/search?' + params.toString()   // ?q=tom+%26+jerry&page=2
The legacy querystring module

Older code (and some libraries) use the built-in node:querystring module. It still works but is considered legacy — prefer URLSearchParams for new code. The main difference: querystring.parse returns a plain object (and uses null-prototype objects), without the rich accessor API:

JS
import querystring from 'node:querystring'

querystring.parse('a=1&b=2&b=3')      // { a: '1', b: ['2', '3'] }  (array for dupes)
querystring.stringify({ a: 1, b: 2 }) // 'a=1&b=2'
Prefer `URLSearchParams`; reach for `querystring` only for legacy interop
`URLSearchParams` is a web standard, available in browsers and Node alike, with a consistent API and proper encoding. The `querystring` module predates it and returns differently-shaped results (object vs iterable, arrays for duplicates). Use `querystring` only when maintaining old code or matching its exact output; otherwise standardize on `URLSearchParams`. See the [querystring module page](/nodejs/querystring-module) for its full API.
Next
Switch sides — make outbound requests from Node as a client: [Making HTTP Requests](/nodejs/http-client).