These libraries handle the conversion between URL query strings and JavaScript objects, a common task in web development. qs is known for robust handling of nested data structures. query-string focuses on modern browser usage with URL synchronization. querystring is a legacy Node.js core module shim. url-parse provides a full URL parser with a focus on performance and simplicity. Choosing the right tool depends on whether you need deep object support, full URL manipulation, or compatibility with older systems.
When building web applications, handling URL query parameters is a daily task. Whether you are reading filters from the address bar or sending data to an API, you need reliable tools to convert between strings and objects. The packages qs, query-string, querystring, and url-parse all tackle this problem, but they serve different purposes and have distinct capabilities. Let's look at how they handle real-world engineering challenges.
Real-world queries often involve nested data, like filters with categories or arrays of IDs. How each library handles this determines if your data survives the round-trip.
qs excels at deep nesting. It supports arrays and objects within the query string by default.
import qs from 'qs';
const query = qs.parse('user[name]=john&user[age]=30');
// Output: { user: { name: 'john', age: '30' } }
const str = qs.stringify({ user: { name: 'john' } });
// Output: 'user%5Bname%5D=john'
query-string supports nested objects but requires specific options to enable brackets notation.
import queryString from 'query-string';
const query = queryString.parse('user[name]=john', { parseNumbers: false });
// Output: { 'user[name]': 'john' } (without arrayFormat/bracket support enabled)
// With newer versions supporting nested parsing:
const queryNested = queryString.parse('user[name]=john', { arrayFormat: 'bracket' });
querystring does not support nested objects natively. It flattens everything into a single-level object.
import querystring from 'querystring';
const query = querystring.parse('user[name]=john');
// Output: { 'user[name]': 'john' } (Key is a string, not an object)
url-parse focuses on the URL structure and exposes the query as a string or simple object, not designed for deep nesting.
import Url from 'url-parse';
const url = new Url('http://example.com?user[name]=john');
const query = url.query;
// Output: '?user[name]=john' (Requires manual parsing for objects)
Sometimes you need to parse the entire URL, including the protocol and hostname. Other times, you only care about the parameters after the ?.
qs works strictly with the query string part. You must extract the query before using it.
import qs from 'qs';
const queryPart = window.location.search.slice(1);
const data = qs.parse(queryPart);
// You handle the URL extraction manually
query-string also focuses on the query string but offers helpers to parse the full URL location.
import queryString from 'query-string';
const data = queryString.parseUrl(window.location.href);
// Output: { url: '...', query: {...}, hash: '...' }
querystring is strictly for the query portion. It has no URL awareness.
import querystring from 'querystring';
const data = querystring.parse(window.location.search.slice(1));
// No URL parsing capabilities included
url-parse is built for full URL manipulation. It breaks down every part of the URL.
import Url from 'url-parse';
const url = new Url('https://example.com/path?query=1');
console.log(url.protocol); // 'https:'
console.log.url.hostname); // 'example.com'
console.log(url.query); // '?query=1'
Special characters in URLs must be encoded correctly to prevent bugs or security issues. Each library has different defaults for handling spaces and special symbols.
qs uses RFC 3986 standards by default, which is safe for most modern APIs.
import qs from 'qs';
const str = qs.stringify({ q: 'hello world' });
// Output: 'q=hello%20world' (Uses %20 for space)
query-string defaults to encoding spaces as +, which is common in forms but sometimes problematic for APIs.
import queryString from 'query-string';
const str = queryString.stringify({ q: 'hello world' });
// Output: 'q=hello+world' (Uses + for space)
querystring also uses + for spaces by default, following older standards.
import querystring from 'querystring';
const str = querystring.stringify({ q: 'hello world' });
// Output: 'q=hello+world'
url-parse relies on standard encoding for the URL parts but leaves query string encoding mostly to the browser or manual handling.
import Url from 'url-parse';
const url = new Url('https://example.com');
url.set('query', 'q=hello world');
// Encoding behavior depends on the browser's URL implementation
Using maintained libraries is critical for security and long-term stability. Some of these packages are legacy tools.
qs is actively maintained and widely used in production systems like Hapi and Express.
// Safe for production use
import qs from 'qs';
query-string is actively maintained and updated for modern JavaScript environments.
// Safe for production use
import queryString from 'query-string';
querystring is deprecated. The Node.js core team marked the original module as legacy, and the npm package is a shim.
// DO NOT USE in new projects
import querystring from 'querystring';
// Warning: Deprecated since Node.js v0.12.0
url-parse is maintained but faces competition from the native URL API.
// Use if native URL API is insufficient
import Url from 'url-parse';
Despite their differences, these libraries share common goals and basic behaviors.
// qs
qs.parse('a=1'); // { a: '1' }
// query-string
queryString.parse('a=1'); // { a: '1' }
// querystring
querystring.parse('a=1'); // { a: '1' }
// url-parse
new Url('?a=1').query; // '?a=1' (String, needs parsing)
// qs
qs.parse('a=1', { allowDots: true });
// query-string
queryString.parse('a=1', { parseNumbers: true }); // { a: 1 }
// querystring
// No built-in type conversion, values remain strings
// url-parse
// No built-in type conversion for query values
// All packages
// Always validate parsed data before using it in logic or DB queries
const data = parseLibrary(input);
if (typeof data.id !== 'string') throw new Error('Invalid ID');
| Feature | qs | query-string | querystring | url-parse |
|---|---|---|---|---|
| Nested Objects | β Excellent Support | β οΈ Limited/Configurable | β No Support | β No Support |
| Full URL Parsing | β Query Only | β οΈ Helper Available | β Query Only | β Full URL |
| Space Encoding | %20 (RFC 3986) | + (Default) | + (Default) | Standard URL |
| Status | β Active | β Active | β Deprecated | β Active |
| Bundle Size | Moderate | Small | Small (Legacy) | Small |
qs is the heavy lifter ποΈββοΈ for complex data. If your API relies on nested query parameters, this is the only safe choice. It ensures your data structure remains intact during transmission.
query-string is the frontend specialist π¨. It is designed for modern web apps where the URL reflects the UI state. Its API is clean and works well with framework routers.
querystring is the legacy tool π°οΈ. It exists for backward compatibility. Do not use it for new development as it lacks features and is no longer maintained.
url-parse is the URL surgeon πͺ. Use it when you need to dissect or modify the entire URL string, not just the parameters. It fills gaps left by the native URL API in specific edge cases.
Final Thought: For most modern frontend work, query-string offers the best balance of features and simplicity. If you are building an API gateway or handling complex filters, qs is indispensable. Avoid querystring entirely in new codebases.
Choose query-string for modern frontend applications that need to sync state with the browser's address bar. It works seamlessly with the History API and handles encoding issues better than native methods in many edge cases. It is lightweight and perfect for single-page applications (SPAs) using React, Vue, or similar frameworks.
Avoid querystring for new projects as it is deprecated and lacks support for nested objects. It is only suitable if you are maintaining legacy Node.js code that relies on the core module shim. For any modern development, migrate to qs or query-string to ensure better security and feature support.
Choose qs when your application deals with complex, nested query parameters, such as filters with multiple levels or arrays of objects. It is the industry standard for safely stringifying and parsing deep structures without losing data fidelity. This package is ideal for backend APIs or heavy-duty form handling where data structure integrity is critical.
Choose url-parse when you need to parse and modify full URLs, not just the query parameters. It is faster than the native URL API in some environments and offers a simpler API for specific mutations. Use this if you are working in environments where the native URL object is unavailable or too heavy for your needs.
Parse and stringify URL query strings
npm install query-string
[!WARNING] Remember the hyphen! Do not install the deprecated
querystringpackage!
For browser usage, this package targets the latest version of Chrome, Firefox, and Safari.
import queryString from 'query-string';
console.log(location.search);
//=> '?foo=bar'
const parsed = queryString.parse(location.search);
console.log(parsed);
//=> {foo: 'bar'}
console.log(location.hash);
//=> '#token=bada55cafe'
const parsedHash = queryString.parse(location.hash);
console.log(parsedHash);
//=> {token: 'bada55cafe'}
parsed.foo = 'unicorn';
parsed.ilike = 'pizza';
const stringified = queryString.stringify(parsed);
//=> 'foo=unicorn&ilike=pizza'
location.search = stringified;
// note that `location.search` automatically prepends a question mark
console.log(location.search);
//=> '?foo=unicorn&ilike=pizza'
Parse a query string into an object. Leading ? or # are ignored, so you can pass location.search or location.hash directly.
The returned object is created with Object.create(null) and thus does not have a prototype.
queryString.parse('?foo=bar');
//=> {foo: 'bar'}
queryString.parse('#token=secret&name=jhon');
//=> {token: 'secret', name: 'jhon'}
Type: object
Type: boolean
Default: true
Decode the keys and values. URL components are decoded with decode-uri-component.
Type: string
Default: 'none'
'bracket': Parse arrays with bracket representation:import queryString from 'query-string';
queryString.parse('foo[]=1&foo[]=2&foo[]=3', {arrayFormat: 'bracket'});
//=> {foo: ['1', '2', '3']}
'index': Parse arrays with index representation:import queryString from 'query-string';
queryString.parse('foo[0]=1&foo[1]=2&foo[3]=3', {arrayFormat: 'index'});
//=> {foo: ['1', '2', '3']}
'comma': Parse arrays with elements separated by comma:import queryString from 'query-string';
queryString.parse('foo=1,2,3', {arrayFormat: 'comma'});
//=> {foo: ['1', '2', '3']}
'separator': Parse arrays with elements separated by a custom character:import queryString from 'query-string';
queryString.parse('foo=1|2|3', {arrayFormat: 'separator', arrayFormatSeparator: '|'});
//=> {foo: ['1', '2', '3']}
'bracket-separator': Parse arrays (that are explicitly marked with brackets) with elements separated by a custom character:import queryString from 'query-string';
queryString.parse('foo[]', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> {foo: []}
queryString.parse('foo[]=', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> {foo: ['']}
queryString.parse('foo[]=1', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> {foo: ['1']}
queryString.parse('foo[]=1|2|3', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> {foo: ['1', '2', '3']}
queryString.parse('foo[]=1||3|||6', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> {foo: ['1', '', 3, '', '', '6']}
queryString.parse('foo[]=1|2|3&bar=fluffy&baz[]=4', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> {foo: ['1', '2', '3'], bar: 'fluffy', baz:['4']}
'colon-list-separator': Parse arrays with parameter names that are explicitly marked with :list:import queryString from 'query-string';
queryString.parse('foo:list=one&foo:list=two', {arrayFormat: 'colon-list-separator'});
//=> {foo: ['one', 'two']}
'none': Parse arrays with elements using duplicate keys:import queryString from 'query-string';
queryString.parse('foo=1&foo=2&foo=3');
//=> {foo: ['1', '2', '3']}
Type: string
Default: ','
The character used to separate array elements when using {arrayFormat: 'separator'}.
Type: Function | boolean
Default: true
Supports both Function as a custom sorting function or false to disable sorting.
Type: boolean
Default: false
import queryString from 'query-string';
queryString.parse('foo=1', {parseNumbers: true});
//=> {foo: 1}
Parse the value as a number type instead of string type if it's a number.
Type: boolean
Default: false
import queryString from 'query-string';
queryString.parse('foo=true', {parseBooleans: true});
//=> {foo: true}
Parse the value as a boolean type instead of string type if it's a boolean.
Type: object
Default: {}
Specifies a schema for parsing query values with explicit type declarations. When defined, the types provided here take precedence over general parsing options such as parseNumbers, parseBooleans, and arrayFormat.
Use this option to explicitly define the type of a specific parameterβparticularly useful in cases where the type might otherwise be ambiguous (e.g., phone numbers or IDs).
You can also provide a custom function to transform the value. The function will receive the raw string and should return the desired parsed result. When used with array formats (like comma, separator, bracket, etc.), the function is applied to each array element individually.
Supported Types:
'boolean': Parse flagged as a boolean (overriding the parseBooleans option):queryString.parse('?isAdmin=true&flagged=true&isOkay=0', {
parseBooleans: false,
types: {
flagged: 'boolean',
isOkay: 'boolean',
},
});
//=> {isAdmin: 'true', flagged: true, isOkay: false}
Note: The 'boolean' type also converts '0' and '1' to booleans, and treats valueless keys (e.g. ?flag) as true.
'string': Parse phoneNumber as a string (overriding the parseNumbers option):import queryString from 'query-string';
queryString.parse('?phoneNumber=%2B380951234567&id=1', {
parseNumbers: true,
types: {
phoneNumber: 'string',
}
});
//=> {phoneNumber: '+380951234567', id: 1}
'number': Parse age as a number (even when parseNumbers is false):import queryString from 'query-string';
queryString.parse('?age=20&id=01234&zipcode=90210', {
types: {
age: 'number',
}
});
//=> {age: 20, id: '01234', zipcode: '90210'}
'string[]': Parse items as an array of strings (overriding the parseNumbers option):import queryString from 'query-string';
queryString.parse('?age=20&items=1%2C2%2C3', {
parseNumbers: true,
types: {
items: 'string[]',
}
});
//=> {age: 20, items: ['1', '2', '3']}
'number[]': Parse items as an array of numbers (even when parseNumbers is false):import queryString from 'query-string';
queryString.parse('?age=20&items=1%2C2%2C3', {
types: {
items: 'number[]',
}
});
//=> {age: '20', items: [1, 2, 3]}
'Function': Provide a custom function as the parameter type. The parameter's value will equal the function's return value. When used with array formats (like comma, separator, bracket, etc.), the function is applied to each array element individually.import queryString from 'query-string';
queryString.parse('?age=20&id=01234&zipcode=90210', {
types: {
age: value => value * 2,
}
});
//=> {age: 40, id: '01234', zipcode: '90210'}
// With arrays, the function is applied to each element
queryString.parse('?scores=10,20,30', {
arrayFormat: 'comma',
types: {
scores: value => Number(value) * 2,
}
});
//=> {scores: [20, 40, 60]}
NOTE: Array types (string[], number[]) are ignored if arrayFormat is set to 'none'.
queryString.parse('ids=001%2C002%2C003&foods=apple%2Corange%2Cmango', {
arrayFormat: 'none',
types: {
ids: 'number[]',
foods: 'string[]',
},
}
//=> {ids:'001,002,003', foods:'apple,orange,mango'}
import queryString from 'query-string';
queryString.parse('?age=20&id=01234&zipcode=90210', {
types: {
age: value => value * 2,
}
});
//=> {age: 40, id: '01234', zipcode: '90210'}
Parse the value as a boolean type instead of string type if it's a boolean.
Stringify an object into a query string and sorting the keys.
Supported value types: string, number, bigint, boolean, null, undefined, and arrays of these types. Other types like Symbol, functions, or objects (except arrays) will throw an error.
Type: object
Type: boolean
Default: true
Strictly encode URI components. It uses encodeURIComponent if set to false. You probably don't care about this option.
Type: boolean
Default: true
URL encode the keys and values.
Type: string
Default: 'none'
'bracket': Serialize arrays using bracket representation:import queryString from 'query-string';
queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'bracket'});
//=> 'foo[]=1&foo[]=2&foo[]=3'
'index': Serialize arrays using index representation:import queryString from 'query-string';
queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'index'});
//=> 'foo[0]=1&foo[1]=2&foo[2]=3'
'comma': Serialize arrays by separating elements with comma:import queryString from 'query-string';
queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'comma'});
//=> 'foo=1,2,3'
queryString.stringify({foo: [1, null, '']}, {arrayFormat: 'comma'});
//=> 'foo=1,,'
// Note that typing information for null values is lost
// and `.parse('foo=1,,')` would return `{foo: [1, '', '']}`.
'separator': Serialize arrays by separating elements with a custom character:import queryString from 'query-string';
queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'separator', arrayFormatSeparator: '|'});
//=> 'foo=1|2|3'
'bracket-separator': Serialize arrays by explicitly post-fixing array names with brackets and separating elements with a custom character:import queryString from 'query-string';
queryString.stringify({foo: []}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> 'foo[]'
queryString.stringify({foo: ['']}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> 'foo[]='
queryString.stringify({foo: [1]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> 'foo[]=1'
queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> 'foo[]=1|2|3'
queryString.stringify({foo: [1, '', 3, null, null, 6]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> 'foo[]=1||3|||6'
queryString.stringify({foo: [1, '', 3, null, null, 6]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|', skipNull: true});
//=> 'foo[]=1||3|6'
queryString.stringify({foo: [1, 2, 3], bar: 'fluffy', baz: [4]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> 'foo[]=1|2|3&bar=fluffy&baz[]=4'
'colon-list-separator': Serialize arrays with parameter names that are explicitly marked with :list:import queryString from 'query-string';
queryString.stringify({foo: ['one', 'two']}, {arrayFormat: 'colon-list-separator'});
//=> 'foo:list=one&foo:list=two'
'none': Serialize arrays by using duplicate keys:import queryString from 'query-string';
queryString.stringify({foo: [1, 2, 3]});
//=> 'foo=1&foo=2&foo=3'
Type: string
Default: ','
The character used to separate array elements when using {arrayFormat: 'separator'}.
Type: Function | boolean
Supports both Function as a custom sorting function or false to disable sorting.
import queryString from 'query-string';
const order = ['c', 'a', 'b'];
queryString.stringify({a: 1, b: 2, c: 3}, {
sort: (a, b) => order.indexOf(a) - order.indexOf(b)
});
//=> 'c=3&a=1&b=2'
import queryString from 'query-string';
queryString.stringify({b: 1, c: 2, a: 3}, {sort: false});
//=> 'b=1&c=2&a=3'
If omitted, keys are sorted using Array#sort(), which means, converting them to strings and comparing strings in Unicode code point order.
Skip keys with null as the value.
Note that keys with undefined as the value are always skipped.
Type: boolean
Default: false
import queryString from 'query-string';
queryString.stringify({a: 1, b: undefined, c: null, d: 4}, {
skipNull: true
});
//=> 'a=1&d=4'
import queryString from 'query-string';
queryString.stringify({a: undefined, b: null}, {
skipNull: true
});
//=> ''
Skip keys with an empty string as the value.
Type: boolean
Default: false
import queryString from 'query-string';
queryString.stringify({a: 1, b: '', c: '', d: 4}, {
skipEmptyString: true
});
//=> 'a=1&d=4'
import queryString from 'query-string';
queryString.stringify({a: '', b: ''}, {
skipEmptyString: true
});
//=> ''
A function that transforms key-value pairs before stringification.
Type: function
Default: undefined
Similar to the replacer parameter of JSON.stringify(), this function is called for each key-value pair and can be used to transform values before they are stringified. The function receives the key and value, and should return the transformed value. Returning undefined will omit the key-value pair from the resulting query string.
This is useful for custom serialization of non-primitive types like Date:
import queryString from 'query-string';
queryString.stringify({
date: new Date('2024-01-15T10:30:00Z'),
name: 'John'
}, {
replacer: (key, value) => {
if (value instanceof Date) {
return value.toISOString();
}
return value;
}
});
//=> 'date=2024-01-15T10%3A30%3A00.000Z&name=John'
You can also use it to filter out keys:
import queryString from 'query-string';
queryString.stringify({
a: 1,
b: null,
c: 3
}, {
replacer: (key, value) => value === null ? undefined : value
});
//=> 'a=1&c=3'
Extract a query string from a URL that can be passed into .parse().
queryString.extract('https://foo.bar?foo=bar');
//=> 'foo=bar'
Extract the URL and the query string as an object.
Returns an object with a url and query property.
If the parseFragmentIdentifier option is true, the object will also contain a fragmentIdentifier property.
import queryString from 'query-string';
queryString.parseUrl('https://foo.bar?foo=bar');
//=> {url: 'https://foo.bar', query: {foo: 'bar'}}
queryString.parseUrl('https://foo.bar?foo=bar#xyz', {parseFragmentIdentifier: true});
//=> {url: 'https://foo.bar', query: {foo: 'bar'}, fragmentIdentifier: 'xyz'}
Type: object
The options are the same as for .parse().
Extra options are as below.
Parse the fragment identifier from the URL.
Type: boolean
Default: false
import queryString from 'query-string';
queryString.parseUrl('https://foo.bar?foo=bar#xyz', {parseFragmentIdentifier: true});
//=> {url: 'https://foo.bar', query: {foo: 'bar'}, fragmentIdentifier: 'xyz'}
Stringify an object into a URL with a query string and sorting the keys. The inverse of .parseUrl()
The options are the same as for .stringify().
Returns a string with the URL and a query string.
Query items in the query property overrides queries in the url property.
The fragmentIdentifier property overrides the fragment identifier in the url property.
queryString.stringifyUrl({url: 'https://foo.bar', query: {foo: 'bar'}});
//=> 'https://foo.bar?foo=bar'
queryString.stringifyUrl({url: 'https://foo.bar?foo=baz', query: {foo: 'bar'}});
//=> 'https://foo.bar?foo=bar'
queryString.stringifyUrl({
url: 'https://foo.bar',
query: {
top: 'foo'
},
fragmentIdentifier: 'bar'
});
//=> 'https://foo.bar?top=foo#bar'
Type: object
Type: string
The URL to stringify.
Type: object
Query items to add to the URL.
Pick query parameters from a URL.
Returns a string with the new URL.
import queryString from 'query-string';
queryString.pick('https://foo.bar?foo=1&bar=2#hello', ['foo']);
//=> 'https://foo.bar?foo=1#hello'
queryString.pick('https://foo.bar?foo=1&bar=2#hello', (name, value) => value === 2, {parseNumbers: true});
//=> 'https://foo.bar?bar=2#hello'
Exclude query parameters from a URL.
Returns a string with the new URL.
import queryString from 'query-string';
queryString.exclude('https://foo.bar?foo=1&bar=2#hello', ['foo']);
//=> 'https://foo.bar?bar=2#hello'
queryString.exclude('https://foo.bar?foo=1&bar=2#hello', (name, value) => value === 2, {parseNumbers: true});
//=> 'https://foo.bar?foo=1#hello'
Type: string
The URL containing the query parameters to filter.
Type: string[]
The names of the query parameters to filter based on the function used.
Type: (key, value) => boolean
A filter predicate that will be provided the name of each query parameter and its value. The parseNumbers and parseBooleans options also affect value.
Type: object
Parse options and stringify options.
This module intentionally doesn't support nesting as it's not spec'd and varies between implementations, which causes a lot of edge cases.
You're much better off just converting the object to a JSON string:
import queryString from 'query-string';
queryString.stringify({
foo: 'bar',
nested: JSON.stringify({
unicorn: 'cake'
})
});
//=> 'foo=bar&nested=%7B%22unicorn%22%3A%22cake%22%7D'
However, there is support for multiple instances of the same key:
import queryString from 'query-string';
queryString.parse('likes=cake&name=bob&likes=icecream');
//=> {likes: ['cake', 'icecream'], name: 'bob'}
queryString.stringify({color: ['taupe', 'chartreuse'], id: '515'});
//=> 'color=taupe&color=chartreuse&id=515'
Sometimes you want to unset a key, or maybe just make it present without assigning a value to it. Here is how falsy values are stringified:
import queryString from 'query-string';
queryString.stringify({foo: false});
//=> 'foo=false'
queryString.stringify({foo: null});
//=> 'foo'
queryString.stringify({foo: undefined});
//=> ''
+ as a space?See this answer.