querystring.parse()

Parses a URL query string into a null-prototype object of decoded key/value pairs.

Signature
querystring.parse(str[, sep[, eq[, options]]])
  • Added in Node.js v0.1.25

Parameters

ParameterTypeDescription
$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>= 1000Specifies 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.url in a bare node:http handler, where no framework has done it for you.
  • Decoding an application/x-www-form-urlencoded request body you have buffered into a string.
  • Splitting any delimited pair list by passing custom sep and eq values.

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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · javascript
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.

Example · javascript
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&note=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.

Example · javascript
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

  • Be the first to comment on this lesson.