querystring.parse()
Parses a URL query string into a null-prototype object of decoded key/value pairs.
querystring.parse(str[, sep[, eq[, options]]])- Added in Node.js v0.1.25
Parameters
| Parameter | Type | Description |
|---|---|---|
$str | <string> | The URL query string to parse. |
$sepoptional | <string>= '&' | The substring used to delimit key and value pairs in the query string. |
$eqoptional | <string>= '=' | The substring used to delimit keys and values in the query string. |
$optionsoptional | <Object> | Options controlling decoding and the key limit. |
$options.decodeURIComponentoptional | <Function>= querystring.unescape() | The function to use when decoding percent-encoded characters in the query string. |
$options.maxKeysoptional | <number>= 1000 | Specifies the maximum number of keys to parse. Specify 0 to remove key counting limitations. |
Return value
<Object>A null-prototype object whose keys are the decoded parameter names and whose values are decoded strings, or arrays of strings when a key appears more than once.
Errors
Does not throw on malformed percent-encoding, because decoding falls back to querystring.unescape(); an error thrown by a custom options.decodeURIComponent is also swallowed — parse() wraps every decoder call in a try/catch and falls back to querystring.unescape() for that key or value.
Changelog
- PHP v8.0.0Multiple empty entries are now parsed correctly (e.g. '&=&=').
- PHP v6.0.0The returned object no longer inherits from Object.prototype, so obj.hasOwnProperty() and obj.toString() are undefined on the result.
- PHP v6.0.0, v4.2.4The eq parameter may now have a length of more than 1.
This is the read half of the module: give it the part of a URL after the ? and you get back plain data. Every value is a string — there is no type coercion, so 'page=2' yields '2', not 2. A key that appears more than once collapses into an array, which means any given property is string | string[] and code that assumes a string will break the first time a client sends the parameter twice.
When to reach for it
- Parsing
req.urlin a barenode:httphandler, where no framework has done it for you. - Decoding an
application/x-www-form-urlencodedrequest body you have buffered into a string. - Splitting any delimited pair list by passing custom
sepandeqvalues.
What trips people up
The returned object has a null prototype. That is a deliberate defence against prototype pollution — a __proto__ key in the input becomes an ordinary property — but it also means result.hasOwnProperty(k) is not a function. Use Object.hasOwn(result, k) or k in result instead.
The maxKeys limit of 1000 silently truncates rather than erroring, so a huge body loses data quietly. Set maxKeys: 0 only when the input is trusted. Finally, pass the query string alone: a leading ? becomes part of the first key.
Example
When to use it
- Reading query parameters out of req.url inside a plain node:http request handler
- Decoding a buffered application/x-www-form-urlencoded POST body into fields
- Turning a stored filter string like 'status=open&owner=me' back into an object
- Splitting a non-standard pair list by passing custom sep and eq delimiters
Frequently asked questions
Why does hasOwnProperty fail on the object returned by querystring.parse?
Because the returned object has a null prototype and therefore inherits no Object methods — use Object.hasOwn(result, key) or the in operator. Node made this change in v6.0.0 so that keys such as __proto__ or constructor in untrusted input cannot reach the prototype chain.
Should I use URLSearchParams instead?
Use URLSearchParams unless parsing is on a hot path — it is the standardized, browser-compatible API, while querystring is faster but Node-specific. URLSearchParams also gives you getAll() for repeated keys instead of the string-or-array shape this function returns.
Why are only 1000 parameters parsed?
The maxKeys option defaults to 1000 and quietly discards everything beyond it, as a denial-of-service guard. Pass { maxKeys: 0 } to remove the limit, but only for input you control.
More examples
Reading query parameters in a node:http handler
Shows the two conversions the parser deliberately does not do for you: numbers arrive as strings and must be coerced, and a repeated key arrives as an array, so normalising with [].concat() keeps the shape stable whether the client sent tag once, twice, or not at all.
const http = require('node:http');
const querystring = require('node:querystring');
http.createServer((req, res) => {
const qIndex = req.url.indexOf('?');
const query = qIndex === -1 ? '' : req.url.slice(qIndex + 1);
// Pass the string WITHOUT the leading '?'.
const params = querystring.parse(query);
// Values are always strings, and may be arrays.
const page = Number(params.page ?? '1');
const tags = [].concat(params.tag ?? []);
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify({ page, tags }));
}).listen(3000);
// GET /search?page=3&tag=node&tag=http
// -> {"page":3,"tags":["node","http"]}Decoding a form-urlencoded request body
The body of an application/x-www-form-urlencoded POST has exactly the same grammar as a query string. The output also shows why the null prototype matters: a '__proto__' field lands as an ordinary own property instead of touching the prototype chain, and Object.hasOwn is the check that still works on such an object.
import querystring from 'node:querystring';
async function readForm(req) {
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const body = Buffer.concat(chunks).toString('utf8');
// maxKeys defaults to 1000 and truncates silently past that.
const fields = querystring.parse(body, '&', '=', { maxKeys: 200 });
// No prototype: Object.hasOwn, not fields.hasOwnProperty.
if (!Object.hasOwn(fields, 'email')) {
throw new Error('email is required');
}
return fields;
}
// Body: email=a%40b.com¬e=hi+there&__proto__=polluted
// -> [Object: null prototype] {
// email: '[email protected]', note: 'hi there', __proto__: 'polluted' }
module.exports = { readForm };Supplying a different decoder for non-UTF-8 input
Passing null for sep and eq keeps the defaults while reaching the fourth argument. The default decoder assumes UTF-8, so %E7 alone would come back as a replacement character; routing decoding through a latin1 decoder recovers the original text.
const querystring = require('node:querystring');
// Legacy endpoints sometimes percent-encode bytes in a non-UTF-8 charset.
function latin1Decode(str) {
const bytes = Buffer.from(str.replace(/\+/g, ' '), 'binary');
return new TextDecoder('latin1').decode(
Buffer.from(unescapeBytes(str)),
);
}
function unescapeBytes(str) {
return str.replace(/\+/g, ' ').split(/(%[0-9A-Fa-f]{2})/).flatMap((part) =>
part.startsWith('%')
? [parseInt(part.slice(1), 16)]
: [...Buffer.from(part, 'utf8')],
);
}
const parsed = querystring.parse(
'name=Fran%E7ois',
null,
null,
{ decodeURIComponent: latin1Decode },
);
console.log(parsed.name); // François
Discussion