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
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)The URLSearchParams API
Method | Returns |
|---|---|
| First value, or |
| Array of all values for repeated keys |
| Boolean — is the key present? |
| Iterators over the pairs |
| Set (replacing existing) |
| Add without replacing |
| Serialize back to a query string |
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
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=2 → getAll('id')). Other ecosystems use a bracket convention (?id[]=1&id[]=2 or ?ids=1,2) — but the URL class does not interpret brackets specially:
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 nestingEncoding and special characters
// ✗ 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=2The 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:
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'