# qs: A Querystring Parser for Node.js with Nested Object Support and Security Limits

> 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.

**ljharb/qs** — A querystring parser and serializer with nesting support

- Repository: https://github.com/ljharb/qs
- Stars: 8,943 · Forks: 938
- Language: JavaScript
- License: BSD-3-Clause
- Published: 2026-09-21 · Updated: 2026-09-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/ljharb-qs

## 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:

```javascript
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' } }`:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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.

## 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.

## FAQ

### 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.

## Sources

- [Issues](https://github.com/ljharb/qs/issues)
- [License: BSD-3-Clause](https://github.com/ljharb/qs/blob/main/LICENSE)
- [ljharb/qs on GitHub](https://github.com/ljharb/qs)
- [README](https://github.com/ljharb/qs/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/ljharb-qs
