qs: A Querystring Parser for Node.js with Nested Object Support and Security Limits
A querystring parser and serializer with nesting support
At a glance
- What is it?
- qs is a BSD-3-Clause JavaScript library that parses and serializes query strings with full support for nested objects, arrays, and configurable security limits on nesting depth and parameter count. It replaces Node.js's built-in querystring module for applications that need bracket notation like foo[bar]=baz.
- Who is it for?
- qs fits any Node.js application that receives query strings with nested objects or arrays, particularly from form submissions or REST API clients that produce bracket notation. The built-in parameterLimit and depth options provide baseline protection; in high-traffic or adversarial environments, set throwOnLimitExceeded to true and bound input at the HTTP layer as well.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 18 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 29, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What qs Solves and Who Uses It
Node.js includes a built-in querystring module and the web-standard URLSearchParams class, both of which parse flat key-value pairs from strings like `a=b&c=d`. Neither supports bracket notation such as `foo[bar]=baz` or `a[0]=x&a[1]=y`, which is how many client libraries and HTML forms represent nested data structures in a URL query string.
qs fills that gap. It parses bracket notation into nested JavaScript objects and arrays, and it serializes JavaScript objects back into that notation. The library has been in use since before it was moved to the ljharb organization, where Jordan Harband took over as lead maintainer. The original author was TJ Holowaychuk.
The library is version 6.16.0 and supports Node.js from v0.6 onward, which covers the entire practical Node.js version range. Browser builds are available in the `dist/` directory of the repository.
Parsing Nested Objects and Handling URI Encoding
The basic parse and stringify calls are straightforward:
var qs = require('qs');
var obj = qs.parse('a=c');
assert.deepEqual(obj, { a: 'c' });
var str = qs.stringify(obj);
assert.equal(str, 'a=c');For nested objects, qs interprets brackets as nested key paths. The string `foo[bar]=baz` produces `{ foo: { bar: 'baz' } }`:
assert.deepEqual(qs.parse('foo[bar]=baz'), {
foo: {
bar: 'baz'
}
});URI-encoded brackets work the same way: `a%5Bb%5D=c` decodes to `{ a: { b: 'c' } }` automatically.
Dot notation is an opt-in alternative to bracket notation, enabled with the `allowDots` option. Passing `allowDots: true` makes `a.b=c` parse as `{ a: { b: 'c' } }`.
The library also supports a custom delimiter. Instead of `&`, you can split on a semicolon or a regular expression covering multiple characters:
var regexed = qs.parse('a=b;c=d,e=f', { delimiter: /[;,]/ });
assert.deepEqual(regexed, { a: 'b', c: 'd', e: 'f' });Depth and Parameter Limits for Input Validation
qs imposes two limits by default to protect against maliciously nested or oversized query strings.
The first is a nesting depth limit, defaulting to 5. A string like `a[b][c][d][e][f][g][h][i]=j` stops parsing bracket groups beyond level 5 and leaves the rest as a string key. The depth can be overridden with the `depth` option:
var deep = qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1 });
assert.deepEqual(deep, { a: { b: { '[c][d][e][f][g][h][i]': 'j' } } });The `strictDepth` option changes this behavior to throw a RangeError when the depth limit is exceeded:
try {
qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1, strictDepth: true });
} catch (err) {
assert(err instanceof RangeError);
}The second limit is a parameter count limit, defaulting to 1000. Additional parameters beyond the limit are silently dropped unless `throwOnLimitExceeded` is set to `true`, which makes qs throw a descriptive error instead.
The README warns that `parameterLimit` does not constrain comma-split values when `comma: true` is enabled. At the default limits of 1000 parameters and an arrayLimit, the total value count can reach 20,000. The README recommends bounding input size at the transport layer as well.
Prototype Pollution: The allowPrototypes Option
By default, qs silently discards any input key that would overwrite a property on Object.prototype. A query string like `a[hasOwnProperty]=b` would otherwise produce an object where `hasOwnProperty` is a string, breaking downstream code that calls it as a method.
The `plainObjects` option bypasses this by returning a null-prototype object:
var nullObject = qs.parse('a[hasOwnProperty]=b', { plainObjects: true });
assert.deepEqual(nullObject, { a: { hasOwnProperty: 'b' } });The `allowPrototypes` option takes a different approach and permits overwriting prototype properties directly. The README's warning is explicit: 'It is generally a bad idea to enable this option as it can cause problems when attempting to use the properties that have been overwritten. Always be careful with this option.'
For most use cases, the default behavior (silently dropping prototype-colliding keys) or `plainObjects` covers the need without the risk that `allowPrototypes` introduces.
qs vs Node.js's Built-in querystring Module
The built-in Node.js querystring module and the web-standard URLSearchParams class handle flat key-value pairs only. A string like `foo[bar]=baz` parses literally as the key `foo[bar]` with value `baz`, with no nesting implied.
qs makes bracket notation meaningful, transforming the same input into `{ foo: { bar: 'baz' } }`. The trade-off is a dependency. For applications that receive or produce query strings from form submissions, REST clients, or libraries that follow PHP's array notation convention, qs's behavior is the correct one. For applications that only handle simple key-value pairs and control both the producer and consumer of the query string, URLSearchParams avoids the dependency entirely.
URLSearchParams is available natively in both Node.js and browsers, requires no installation, and handles encoding correctly for its scope. qs's value proposition is purely the nested-structure interpretation.
Ignoring Query Prefixes and Handling Leading Question Marks
Some consumers of qs receive a full URL query component including the leading `?`. Passing that string directly to `qs.parse` would include `?` as part of the first key. The `ignoreQueryPrefix` option strips the leading question mark before parsing:
var prefixed = qs.parse('?a=b&c=d', { ignoreQueryPrefix: true });
assert.deepEqual(prefixed, { a: 'b', c: 'd' });This option is a quality-of-life feature for code that extracts the query portion of a URL and passes it directly to qs without trimming the `?`. Without it, the first key would be `?a` instead of `a`.
Maintenance and Licensing
qs is BSD-3-Clause licensed. The current version is 6.16.0 and the last push to the main branch was on 2026-09-12. The project has the OpenSSF Best Practices badge, which indicates it meets a set of security and maintenance criteria.
The repository uses `side-channel` and `es-define-property` as runtime dependencies. Both are small utility packages maintained by the same lead maintainer, Jordan Harband, which reduces the risk of dependency drift. The package.json shows a minimum Node.js version of v0.6, providing broad compatibility.
The project includes a bower.json and component.json for legacy package manager support, and a dist/ directory with a browser build. The CHANGELOG.md tracks version changes; the README notes that since v6.14.1 and v6.15.2, parsing of unbalanced brackets changed, which may affect applications upgrading from 6.15.1.
The qs library also supports stringification of arrays, with options for bracket notation, indexed notation, or comma notation. The `comma` option on stringify joins array values with commas into a single parameter value. When combined with `throwOnLimitExceeded: true`, the library enforces all configured limits strictly, including `arrayLimit` which controls the maximum array index qs will create during parsing. Any index at or above the `arrayLimit` value causes the parser to treat that key as a plain string rather than an array index, which is the behavior that prevents memory exhaustion from inputs like `a[99999999]=1`. These limits operate independently per array, meaning a query string with many small arrays can still reach the product of `parameterLimit` and `arrayLimit` in total values at default settings.
Editorial conclusion
qs fits any Node.js application that receives query strings with nested objects or arrays, particularly from form submissions or REST API clients that produce bracket notation. The built-in parameterLimit and depth options provide baseline protection; in high-traffic or adversarial environments, set throwOnLimitExceeded to true and bound input at the HTTP layer as well. Projects that only handle flat key-value pairs can stay with URLSearchParams and skip the dependency.
Frequently asked questions
What is qs in Node.js?
qs is a querystring parsing and serializing library that extends Node.js's basic query string handling to support nested objects and arrays using bracket notation. It also adds configurable security limits on nesting depth and parameter count.
What is the default depth limit in qs.parse and why does it exist?
The default depth limit is 5 levels of nested brackets. It exists to mitigate abuse from deeply nested input that can cause excessive memory allocation or slow processing. Set the depth option to override it, and set strictDepth to true to throw an error instead of silently truncating.
Does qs protect against prototype pollution?
By default, qs silently drops any key that would overwrite a property on Object.prototype, such as hasOwnProperty or toString. The plainObjects option returns a null-prototype object instead, preserving those keys safely. The allowPrototypes option disables both protections and is documented as dangerous.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/ljharb-qs)