Library / SDK
sindresorhus/query-string avatar
sindresorhus/query-string

query-string: seven array formats, a types schema, and no CommonJS entry point

Parse and stringify URL query strings

6,918 stars458 forksJavaScriptMIT

At a glance

What is it?
The npm package that parses and stringifies URL query strings makes array parsing an explicit choice, keeps type coercion off by default, and returns an object with no prototype. It is ESM only, and the README itself points you at URLSearchParams for simple cases.
Who is it for?
Adopt query-string when your URLs carry arrays, encoded values or keys that must stay strings, because URLSearchParams handles the simple cases and this package covers the rest. Do not adopt it in a CommonJS build that cannot consume ES modules, or on a runtime older than Node 22.
Can I use it commercially?
Yes. MIT 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 24 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 13, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • sindresorhus/query-string6,918 ★ · +9
Star history
Repository2026-09-232026-11-13
sindresorhus/query-string6.9k6.9k

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

Editorial analysis

Seven array formats, and a default of repeated keys

Query string libraries disagree about how an array should be encoded, so this one makes you say which convention you mean. The arrayFormat option takes seven documented values and defaults to 'none', which reads arrays out of repeated keys: foo=1&foo=2&foo=3 produces an array. Switch it to 'bracket' and the same result comes from foo[]=1&foo[]=2&foo[]=3. The 'index' format expects foo[0]=1&foo[1]=2&foo[3]=3. 'comma' splits on a comma inside one value, and 'separator' splits on a character you name in arrayFormatSeparator, whose default is ','. Two of the seven are less common. 'bracket-separator' applies only to keys explicitly marked with brackets and accepts empty elements, which is why foo[]=1||3|||6 is a meaningful input. 'colon-list-separator' expects parameter names marked with :list, so foo:list=one&foo:list=two becomes an array of two. Nothing in this option set parses nested objects or JSON payloads.

js
queryString.parse('foo[]=1&foo[]=2&foo[]=3', {arrayFormat: 'bracket'});
//=> {foo: ['1', '2', '3']}

queryString.parse('foo=1,2,3', {arrayFormat: 'comma'});
//=> {foo: ['1', '2', '3']}

parseNumbers and parseBooleans ship switched off

Every value arrives as a string unless something asks otherwise, and the two switches that do the asking both default to false. Leaving them off is what keeps the values people get wrong intact: id=1 stays a string, zipcode=90210 keeps its digits as written, and 01234 is not quietly rounded into a number. Switch on parseNumbers and a numeric string becomes a number. The plain flags have no per key granularity, so a query string that mixes a numeric id with a zero padded code needs the schema below or manual conversion at the call site.

js
queryString.parse('foo=1', {parseNumbers: true});
//=> {foo: 1}

The types schema outranks the general parsing flags

When a key is ambiguous, the types option takes precedence over parseNumbers, parseBooleans and arrayFormat. It is an object that maps parameter names to a type name and it defaults to an empty object, so nothing changes until you declare something. The README's comma example declares 'string[]' so that a single value, foo=1, still comes back as an array, ['1'].

js
queryString.parse('foo=1', {arrayFormat: 'comma', types: {foo: 'string[]'}});
//=> {foo: ['1']}

Decoding belongs to decode-uri-component, and it is a real dependency

Keys and values are decoded by default, and the decode option is the switch that turns it off. The README says invalid UTF-8 bytes become U+FFFD, like the URL parser and URLSearchParams, and a % not followed by two hexadecimal digits is kept as it is. The runtime dependencies are filter-obj at ^6.1.0 and split-on-first at ^4.0.0. Two entries is a short list for something that touches every URL an application handles, and the published payload is equally small: the files field ships exactly four files, index.js, index.d.ts, base.js and base.d.ts, which is an entry point, its type definition, and a second JavaScript file beside them. If your team reviews transitive dependencies, this is a list you can read in a minute rather than an audit you schedule.

The parse result has no prototype, which cuts both ways

The object handed back by parse is created with Object.create(null), so it has no prototype at all. Two things follow from that. A query parameter called constructor or toString becomes an ordinary own property instead of colliding with an inherited member, which is the reason for a prototype-less result in the first place. The cost lands on your side of the API. There is no inherited hasOwnProperty to call, so a membership check written for an ordinary object does not work, and anything that assumes a plain object, a serialiser, a merge helper, a lint rule, meets something unusual on the way past. Values are still strings unless you asked otherwise, and sorting is a separate option that accepts either a function or false to switch it off, documented alongside the other parse options. A leading question mark and a leading hash are both ignored, so location.search and location.hash can be passed in unchanged.

js
queryString.parse('?foo=bar');
//=> {foo: 'bar'}

queryString.parse('#token=secret&name=john');
//=> {name: 'john', token: 'secret'}

ESM only, Node 18 as the floor, and four shipped files

package.json declares type module, and the exports map carries exactly two conditions: types pointing at ./index.d.ts and default pointing at ./index.js. There is no require condition and no main field, so a CommonJS consumer has nothing to require and needs a dynamic import or a build step that understands ES modules. The engines field asks for Node 22 or newer, and for browser use the README targets the latest Chrome, Firefox and Safari, which is a statement about evergreen releases rather than about older engines anyone still has to support. sideEffects is set to false, so a bundler may drop an unused import without asking you. Types are treated as part of the contract here rather than an afterthought: index.test-d.ts sits at the top level, and the test script is xo && node --test && nz.

json
	"type": "module",
	"exports": {
		"types": "./index.d.ts",
		"default": "./index.js"
	},
	"sideEffects": false,
	"engines": {
		"node": ">=22"
	},

One npm line to install, and three gates that run before you publish

Installation is a single line, and two notes sit next to it. Keep the hyphen, because the similarly named querystring package is deprecated and is a different project by a different author. And consider the native URLSearchParams instead if your case really is simple, since that is what the platform gives you for free. For arrays, explicit types and predictable decoding, this is the package you install.

sh
npm install query-string

Your first call parses whatever the browser handed you.

js
import queryString from 'query-string';

const parsed = queryString.parse(location.search);
//=> {foo: 'bar'}

Stringify turns the object back into a query string, and assigning the result to location.search leaves the question mark to the browser. On the project's own quality, the test script is xo && node --test && nz, so linting and the Node test runner run in one command, with fast-check and deep-equal in devDependencies. A benchmark script, node --experimental-bench --bench benchmark.js, and a benchmark.js at the top level exist for local comparisons, and there is no changelog file among the top level entries. Recent releases are v10.0.0 on 2026-10-21, v9.5.1 on 2026-09-01 and v9.5.0 on 2026-08-06, with the last push to the repository on 2026-10-21.

Editorial conclusion

Adopt query-string when your URLs carry arrays, encoded values or keys that must stay strings, because URLSearchParams handles the simple cases and this package covers the rest. Do not adopt it in a CommonJS build that cannot consume ES modules, or on a runtime older than Node 22. Before wiring the types schema, check the arrayFormat your own API actually emits, since the default reads repeated keys and nothing else.

Frequently asked questions

How do I access a query string in JavaScript with query-string?

Pass location.search or location.hash straight to parse, since a leading ? or # is ignored. The result is an object built with Object.create(null), so it has no prototype and no inherited hasOwnProperty. Values are strings unless you enable parseNumbers or parseBooleans, which both default to false.

How do I add a query string to a URL with query-string?

Call stringify on the object and you get the query string back, for example 'foo=unicorn&ilike=pizza' from two keys. Assigning that result to location.search works too, and the browser prepends the question mark for you, so you should not add one yourself.

What does a query string parameter become in query-string parse output?

The parameter name becomes an object key and the value a string. Repeated keys become an array under the default arrayFormat of 'none', and seven formats are documented for reading arrays written as foo[]=1, foo[0]=1, comma separated values, a custom separator, bracket separators, :list names, or repeated keys.

What is a query string in a URL, and how does query-string parse it?

In this package the query string is the part after ? or #, and parse takes that whole string and returns an object, ignoring the leading character. Decoding of keys and values is on by default, and the decode option switches it off.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. sindresorhus/query-string on GitHub
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/sindresorhus-query-string.svg)](https://hysenlabs.com/projects/sindresorhus-query-string)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/sindresorhus-query-string.svg)](https://hysenlabs.com/projects/sindresorhus-query-string)

Was this useful?