These libraries handle different aspects of URL processing in JavaScript applications. query-string focuses strictly on parsing and stringifying the query portion of a URL (the part after ?). url-parse breaks down a complete URL into its components like protocol, host, and pathname. url-parse-lax is a wrapper around url-parse that tolerates missing protocols, making it useful for user-generated input. url-search-params-polyfill provides a fallback implementation of the native URLSearchParams API for older environments that lack support.
Working with URLs is a daily task in frontend development. Whether you are reading filters from the address bar, validating user input, or constructing API links, you need reliable tools. The packages query-string, url-parse, url-parse-lax, and url-search-params-polyfill each solve specific parts of this puzzle. Let's look at how they differ and where they fit in your stack.
The first decision is whether you need to parse the entire address or just the parameters.
query-string works only on the search part. It ignores the domain and protocol. You pass it the string that comes after the ?.
import queryString from 'query-string';
const params = queryString.parse('foo=1&bar=2');
// Result: { foo: '1', bar: '2' }
url-parse handles the complete address. It splits the protocol, host, port, and path into an object.
import UrlParse from 'url-parse';
const url = new UrlParse('https://example.com/path?foo=1');
// Result: { protocol: 'https:', hostname: 'example.com', query: '?foo=1' }
url-parse-lax also handles the complete address but is more forgiving. It accepts strings without a protocol.
import urlParseLax from 'url-parse-lax';
const url = urlParseLax('example.com/path?foo=1');
// Result: { protocol: 'https:', hostname: 'example.com', ... }
url-search-params-polyfill does not parse URLs itself. It adds the URLSearchParams class to the global scope so you can use the native API on old browsers.
import 'url-search-params-polyfill';
const params = new URLSearchParams('foo=1&bar=2');
// Result: URLSearchParams object with native methods
Users often paste links without the http:// or https:// part. Standard parsers often fail or treat the domain as a path.
query-string does not handle this. It expects a clean query string. If you pass a full URL, it will likely return empty or incorrect results.
import queryString from 'query-string';
// Incorrect usage for full URLs
const params = queryString.parse('example.com?foo=1');
// Result: { 'example.com?foo': '1' } (Treats domain as key)
url-parse requires a valid protocol. Without it, the hostname might end up in the pathname field.
import UrlParse from 'url-parse';
const url = new UrlParse('example.com/path');
// Result: { hostname: '', pathname: 'example.com/path' }
url-parse-lax is built exactly for this case. It assumes https:// if no protocol is found.
import urlParseLax from 'url-parse-lax';
const url = urlParseLax('example.com/path');
// Result: { protocol: 'https:', hostname: 'example.com', pathname: '/path' }
url-search-params-polyfill relies on the URL constructor or string input for URLSearchParams. If the input is not a valid query string, it will not fix the missing protocol issue.
import 'url-search-params-polyfill';
// Fails if passed a full URL without extracting search first
const params = new URLSearchParams('example.com?foo=1');
// Result: Treats 'example.com' as a key
Turning data back into a URL string is where encoding rules matter. Special characters must be escaped correctly.
query-string gives you control over encoding. It handles arrays and objects gracefully by default.
import queryString from 'query-string';
const str = queryString.stringify({ q: 'hello world' });
// Result: 'q=hello+world'
url-parse does not stringify query objects directly. You usually update the .query property and call .toString().
import UrlParse from 'url-parse';
const url = new UrlParse('https://example.com');
url.query = { q: 'hello world' }; // This often requires manual stringifying
// Better: url.set('query', 'q=hello+world');
url-parse-lax inherits the behavior of url-parse. It is primarily for parsing, not building strings from scratch.
import urlParseLax from 'url-parse-lax';
// Primarily a parser, not a stringifier
const url = urlParseLax('example.com');
// To build: use .toString() after modification
url-search-params-polyfill enables the native .toString() method on the URLSearchParams instance.
import 'url-search-params-polyfill';
const params = new URLSearchParams();
params.append('q', 'hello world');
const str = params.toString();
// Result: 'q=hello+world'
Browser support changes how we choose tools. Modern browsers have built-in APIs that make some packages redundant.
query-string is actively maintained. It fills gaps where the native API is verbose or behaves differently regarding arrays and sorting.
// Native API requires more code for arrays
const params = new URLSearchParams();
params.append('tag', 'js');
params.append('tag', 'node');
url-parse is stable and widely used in Node.js and browser environments where a lightweight parser is needed without the full Node url module.
// Node native 'url' module is heavier in some bundlers
import UrlParse from 'url-parse'; // Lightweight alternative
url-parse-lax is useful but niche. If you control your input sources, you might not need the lax behavior.
// If input is trusted, standard url-parse is safer
import UrlParse from 'url-parse';
url-search-params-polyfill is largely obsolete. All modern evergreen browsers support URLSearchParams natively. Using this adds weight for no gain in 2024+.
// Modern approach: No import needed
const params = new URLSearchParams(window.location.search);
| Feature | query-string | url-parse | url-parse-lax | url-search-params-polyfill |
|---|---|---|---|---|
| Input Scope | Query string only | Full URL | Full URL (incomplete OK) | Query string (via Global) |
| Missing Protocol | ❌ N/A | ❌ Fails | ✅ Auto-adds https | ❌ N/A |
| Stringify | ✅ Built-in | ⚠️ Via .toString() | ⚠️ Via .toString() | ✅ Native .toString() |
| Modern Need | ✅ High (DX) | ✅ Medium | ⚠️ Low (Specific) | ❌ None (Native) |
| Array Support | ✅ Excellent | ⚠️ Manual | ⚠️ Manual | ✅ Native |
For most modern frontend applications, start with the native URL and URLSearchParams APIs. They are built into the browser and require no dependencies. If you need better developer experience for handling arrays or encoding options, query-string is the best addition. It is lightweight and solves common pain points without reinventing the wheel.
Use url-parse if you are in an environment where the native URL object is unavailable or too heavy, such as older Node.js versions or specific serverless runtimes. Use url-parse-lax only if you are building a tool that accepts raw user input like a link shortener or a dashboard where users paste incomplete URLs.
Avoid url-search-params-polyfill in new projects. The cost of supporting Internet Explorer 11 is no longer justified for most teams. Rely on the native implementation and let your build tools handle transpilation if absolutely necessary. Choose tools that match your actual browser support matrix — do not carry legacy weight into modern codebases.
Choose query-string when you only need to work with the query parameters of a URL and not the full address. It is ideal for scenarios like reading filters from a browser address bar or building search links where you already have the base path. It offers robust handling of arrays and encoding options that the native API sometimes handles differently.
Choose url-parse when you need to deconstruct a complete URL into its parts, such as extracting the hostname or port from a full string. It is suitable for server-side rendering logic, proxy configurations, or validation tasks where the protocol and domain matter as much as the path.
Choose url-parse-lax when dealing with untrusted or incomplete user input that might lack a protocol scheme (like http://). It is best for input fields where users paste links without thinking about the scheme, ensuring your parser does not crash on valid but incomplete URLs.
Choose url-search-params-polyfill only if you must support legacy browsers like Internet Explorer 11 that do not have the native URLSearchParams API. For all modern projects targeting evergreen browsers, this package is unnecessary and should be avoided in favor of the built-in web standard.
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.
[!TIP] Consider using
URLSearchParamsfor simple use cases. It's a native browser API that handles basic query string operations.
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.