Encoding and Decoding Query Strings in Node.js
Node.js gives you two built-in ways to build and parse query strings: URLSearchParams and the older querystring module. Both handle percent-encoding for you automatically — but they behave differently enough with arrays and special characters that picking the wrong one causes bugs. Here's how to use each.
Building a query string with URLSearchParams
URLSearchParams is the modern, web-standard way to build query strings — it works identically in Node.js and browsers. Pass it a plain object or array of pairs, and it handles encoding automatically.
const params = new URLSearchParams({
q: 'node.js & express',
page: '2',
sort: 'desc',
});
console.log(params.toString());
// → 'q=node.js+%26+express&page=2&sort=desc'
const url = `https://api.example.com/search?${params.toString()}`;Parsing a query string with URLSearchParams
import { URL } from 'url';
const url = new URL('https://api.example.com/search?q=node.js+%26+express&page=2');
url.searchParams.get('q'); // 'node.js & express' — decoded automatically
url.searchParams.get('page'); // '2'
url.searchParams.has('sort'); // false
// Or parse a bare query string directly:
const params = new URLSearchParams('q=hello+world&page=3');
params.get('q'); // 'hello world'Handling repeated keys (arrays)
A common source of bugs: using a plain object with an array value doesn't produce repeated key=value pairs automatically. Use .append() in a loop instead, and .getAll() to read them back.
const params = new URLSearchParams();
for (const tag of ['nodejs', 'backend', 'api']) {
params.append('tag', tag);
}
console.log(params.toString());
// → 'tag=nodejs&tag=backend&tag=api'
// Reading them back
const parsed = new URLSearchParams('tag=nodejs&tag=backend&tag=api');
parsed.getAll('tag');
// → ['nodejs', 'backend', 'api']The querystring module (legacy, still works)
The older querystring module is still built into Node.js. It handles arrays natively (unlike a plain object passed to URLSearchParams), which is occasionally convenient, but Node's own documentation recommends URLSearchParams for new code.
import querystring from 'querystring';
// Build — arrays work directly, unlike URLSearchParams(object)
const qs = querystring.stringify({ tag: ['nodejs', 'backend'], page: 2 });
console.log(qs);
// → 'tag=nodejs&tag=backend&page=2'
// Parse
const parsed = querystring.parse('tag=nodejs&tag=backend&page=2');
console.log(parsed);
// → { tag: ['nodejs', 'backend'], page: '2' }Using query strings in an Express route
import express from 'express';
const app = express();
// GET /search?q=node.js%20tutorial&page=2
app.get('/search', (req, res) => {
// Express parses req.query for you using the querystring module internally
const { q, page = '1' } = req.query;
res.json({ query: q, page: Number(page) });
});
// Building a link to another endpoint with query params
app.get('/next-page-link', (req, res) => {
const params = new URLSearchParams({ q: req.query.q, page: '3' });
res.json({ next: `/search?${params.toString()}` });
});URLSearchParams vs querystring — quick comparison
- URLSearchParams — web standard, works in browsers too, recommended by Node.js docs, cleaner iteration API.
- querystring — Node-only, marked legacy, but handles array values from a plain object without extra loops.
- Both automatically percent-encode and decode values — you should rarely call
encodeURIComponentmanually when using either.
Frequently Asked Questions
Prefer URLSearchParams for new code — it is a web standard also available in browsers, has a cleaner API, and is actively maintained. The querystring module still works and is slightly faster for simple cases, but Node.js docs mark it as legacy in favor of URLSearchParams.
Pass the object directly to new URLSearchParams(obj) and call .toString(). URLSearchParams automatically percent-encodes keys and values for you, so you never need to call encodeURIComponent manually.
Use URLSearchParams.append() to add multiple values under the same key, and .getAll(key) to retrieve them all as an array. Using .set() or a plain object will overwrite earlier values instead of preserving duplicates.