Library / SDK
ljharb/qs avatar
ljharb/qs

ljharb/qs: a querystring parser that turns bracket notation into nested objects

A querystring parser and serializer with nesting support

8,951 stars983 forksJavaScriptBSD-3-Clause

At a glance

What is it?
qs parses and serializes query strings with nesting, arrays and a configurable depth limit. It suits server-side code that has to read structured input from a URL, and it is the wrong pick if you only need flat key-value pairs.
Who is it for?
Adopt qs if you are parsing structured query strings in Node or a browser bundle and you want bracket nesting, array handling and limits you can tune. Do not adopt it for a single flat key-value pair, and do not enable allowPrototypes on user input without understanding that it lets input overwrite prototype properties.
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 16 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 November 14, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • ljharb/qs8,951 ★ · +11
Star history
Repository2026-09-222026-11-14
ljharb/qs8.9k9k

11 Stars in the last 53 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

The problem qs solves: structured input arriving as a flat string

A query string is a flat sequence of key-value pairs separated by ampersands. Most of the time that is all you need, and Node's built-in querystring module or the URLSearchParams API is enough. qs exists for the cases where the client sends more structure than the format natively holds. A form that submits an array, a filter UI that sends nested conditions, or any client that follows the bracket convention PHP popularized will produce strings like foo[bar]=baz or a%5Bb%5D=c. Parsing those by hand means writing a small recursive parser and getting the edge cases wrong.

The audience is server-side JavaScript. The README describes the module as a querystring parsing and stringifying library with some added security, and the package description adds that it supports nesting and arrays with a depth limit. The library is also published for browsers through a dist directory in the repository, so client-side code can use the same API. The lead maintainer is Jordan Harband, and the README credits TJ Holowaychuk with creating the original module.

How bracket notation becomes a nested object

The core mechanism is a recursive descent over the string. qs splits the input on the delimiter, then examines each key for square brackets. A key with no brackets is assigned directly. A key like foo[bar] causes the parser to create an object at foo and place the value under bar. Deeper forms such as foo[bar][baz]=foobarbaz produce a matching chain of objects. The README shows that URI-encoded brackets work the same way, so a%5Bb%5D=c parses to { a: { b: 'c' } }.

Two defaults shape what comes out. Nesting stops at five children deep, and anything past that point is kept as a literal string key. The README gives the example a[b][c][d][e][f][g][h][i]=j, which produces a chain down to f and then a single key '[g][h][i]' holding j. Parsing also stops at 1000 parameters unless parameterLimit is raised. Both numbers are defensible defaults for untrusted input, and both are silent by default: qs parses what it can and drops or flattens the rest. The strictDepth and throwOnLimitExceeded options change that, turning the limit into a thrown error instead. The README notes that strictDepth throws a RangeError with the message Input depth exceeded depth option of 1 and strictDepth is true, and that throwOnLimitExceeded produces a message like Parameter limit exceeded. Only 3 parameters allowed.

One detail in the README deserves attention. parameterLimit bounds the number of ampersand-delimited parameters, not the number of values those parameters expand into. With comma splitting enabled, a single parameter can become arbitrarily many array elements. That is an honest limitation stated by the project rather than a flaw discovered from the outside.

Installing qs and parsing a nested query string

The package is published on npm under the name qs. Installation is a normal npm install, and the README's usage example is CommonJS, so require works without any build step.

bash
npm install qs

The next block is the README's own starting example. It parses a single pair and then serializes the result back, asserting that the round trip returns the original string.

javascript
var qs = require('qs');
var assert = require('assert');

var obj = qs.parse('a=c');
assert.deepEqual(obj, { a: 'c' });

var str = qs.stringify(obj);
assert.equal(str, 'a=c');

A more representative first use is a nested string. The README shows that foo[bar]=baz becomes { foo: { bar: 'baz' } }. If you want to see the depth limit in action, pass a low depth and inspect the result: the README's example parses a[b][c][d][e][f][g][h][i]=j with depth set to 1 and gets { a: { b: { '[c][d][e][f][g][h][i]': 'j' } } }. The remainder of the key stays as a literal string, which is the behaviour to expect whenever input goes deeper than you allow.

To reject over-deep input instead of flattening it, add strictDepth. The README's example catches a RangeError.

javascript
try {
    qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1, strictDepth: true });
} catch (err) {
    assert(err instanceof RangeError);
}

Prototype pollution and the options that guard against it

The safety story is the part of qs most worth reading before you enable anything. By default, parameters that would overwrite properties on the object prototype are ignored. That default is the reason the library is safe to point at user input out of the box.

Two options change the outcome. plainObjects returns the parsed value as a null-prototype object created via { __proto__: null }, so prototype methods do not exist on it and a user can set names like hasOwnProperty to whatever they like. The README shows a[hasOwnProperty]=b parsing to { a: { hasOwnProperty: 'b' } } under that option. The alternative is allowPrototypes, which lets user input overwrite prototype properties on an ordinary object. The README attaches a warning to it: it is generally a bad idea to enable this option, because it can cause problems when code later tries to use the properties that were overwritten.

My reading is that plainObjects is the better of the two when you need to preserve such keys. It keeps the data without mutating a shared prototype. allowPrototypes should be treated as a compatibility switch for legacy input, not as a default. If you are changing this setting on a live service, compare parsed output before and after rather than assuming the difference is limited to the keys you care about.

Where qs is the wrong tool

If your query string is flat, qs is more machinery than the job needs. Node's querystring module and the WHATWG URLSearchParams API both handle key-value pairs without the bracket grammar, the depth limit or the prototype options, and neither adds a dependency. The repository lists two runtime dependencies, es-define-property and side-channel, which is small but still larger than zero.

The second case is input you do not control and cannot bound. qs gives you depth, parameter and array limits, but the README is explicit that parameterLimit does not constrain comma-split expansion. If you cannot bound input size before the parser, the limits inside qs are not a complete defence.

Third, version behaviour around brackets changed. Since v6.14.1 and v6.15.2, parse interprets nested bracket groups more consistently, and the README warns that query strings with unbalanced [ or ] can produce different results than in 6.15.1, pointing to issue #558. If you store URLs with malformed brackets and parse them later, an upgrade can change what you get back. That is a real upgrade cost, not a hypothetical one.

qs compared with Node's built-in querystring

The closest alternative is the querystring module that ships with Node. The difference is scope, not quality. querystring.parse returns a flat object: repeated keys become arrays, and bracket characters are treated as ordinary text in the key name. It does not build nested objects, it has no depth option, and it has no equivalent of plainObjects or allowPrototypes because there is no nesting step that could reach the prototype.

qs adds the recursive layer and the limits that come with it. The cost is that you now have options to reason about and defaults that silently reshape input. The benefit is that a client using bracket notation gets the structure it intended without custom parsing code on your side. If your clients send flat pairs, querystring is the smaller answer. If they send nested filters or arrays, qs is doing work you would otherwise write yourself.

Maintenance, licence and upgrade cost

The repository is not archived, and the last push was on 2026-10-28, which is recent. The licence is BSD-3-Clause, a permissive licence that allows use in closed-source products provided the copyright notice and licence text are retained. That is a statement about the licence text, not legal advice; if your organisation has specific obligations around attribution, read LICENSE.md in the repository.

The package.json declares engines.node as >=0.6, an unusually wide range that reflects the library's age. Two runtime dependencies are listed, and the devDependencies include browserify, eslint and nyc, which suggests the published dist bundle is built and tested in CI. The repository has no published GitHub releases, so version-specific changes are known only from the README's mention of v6.14.1 and v6.15.2 and the current version of 6.17.1 in package.json. The upgrade cost that matters is the bracket parsing change described above.

Editorial conclusion

Adopt qs if you are parsing structured query strings in Node or a browser bundle and you want bracket nesting, array handling and limits you can tune. Do not adopt it for a single flat key-value pair, and do not enable allowPrototypes on user input without understanding that it lets input overwrite prototype properties. Before shipping, verify the behaviour of your own query strings at the default depth of 5 and parameterLimit of 1000, and check the 6.14.1 and 6.15.2 change to bracket group parsing if you have unbalanced brackets in stored URLs.

Frequently asked questions

What is qs in Node?

qs is a querystring parsing and stringifying library. The README describes it as having added security features, and the package description says it supports nesting and arrays with a depth limit.

How do I install qs?

It is published on npm as qs, so npm install qs is the installation step. The README's usage example then loads it with require('qs').

How deep does qs parse nested objects by default?

By default qs parses up to 5 children deep. Anything deeper is kept as a literal string key, as the README's example with a[b][c][d][e][f][g][h][i]=j shows.

How many parameters does qs parse before stopping?

By default qs parses up to 1000 parameters, and the limit can be changed with the parameterLimit option. With throwOnLimitExceeded set to true, exceeding a limit raises an error instead of being ignored.

Does qs protect against prototype pollution?

By default, parameters that would overwrite properties on the object prototype are ignored. The plainObjects option returns a null-prototype object instead, while allowPrototypes lets input overwrite prototype properties and carries a warning in the README.

Official sources

  1. Issues
  2. License: BSD-3-Clause
  3. ljharb/qs on GitHub
  4. README
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/ljharb-qs.svg)](https://hysenlabs.com/projects/ljharb-qs)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/ljharb-qs.svg)](https://hysenlabs.com/projects/ljharb-qs)

Was this useful?