path-to-regexp、query-string、uri-template 和 url-template 都是处理 URL 相关逻辑的核心工具库,但侧重点不同。path-to-regexp 是路由匹配的事实标准,擅长将 URL 路径映射到处理函数;query-string 专注于解析和序列化 URL 查询参数;uri-template 和 url-template 则实现了 RFC 6570 标准,用于根据模板动态生成 URL。在现代前端架构中,它们通常组合使用,但在特定场景下(如纯路由或纯参数处理)需要做出明确的技术选型。
在构建复杂的前端或 Node.js 应用时,URL 处理是基础设施中的关键一环。path-to-regexp、query-string、uri-template 和 url-template 分别解决了 URL 处理的不同切片。本文将深入对比它们在路径匹配、参数解析和模板生成方面的技术细节,帮助架构师做出准确决策。
这是路由系统的核心功能。我们需要从 URL 中提取动态段(如 /user/123 中的 123)。
path-to-regexp 是此领域的王者。它支持强大的匹配语法(如正则、可选参数)。
import { match } from 'path-to-regexp';
const matchUser = match('/user/:id');
const result = matchUser('/user/123');
// result.params.id === '123'
query-string 专注于查询字符串,不支持 路径段匹配。强行使用它处理路径会导致逻辑错误。
import queryString from 'query-string';
// 仅能解析 ? 之后的内容,无法处理 /user/:id
const parsed = queryString.parse('?id=123');
// parsed.id === '123',但无法从路径中提取
uri-template 主要用于模板展开,虽然理论上可解析,但 API 设计侧重于生成而非匹配路径。
import { UriTemplate } from 'uri-template';
// 主要用于 expand,match 功能有限或非核心设计
const template = new UriTemplate('/user/{id}');
// 通常用于生成 URL,而非路由匹配
url-template 同样遵循 RFC 6570,侧重于根据变量生成 URL,而非作为路由器解析传入请求。
import urlTemplate from 'url-template';
// 侧重于 expand 方法
const template = urlTemplate.parse('/user/{id}');
// 设计初衷是生成链接,而非匹配路由
处理 ?key=value&list=1&list=2 这种逻辑是前端常见需求。
path-to-regexp 不处理查询参数。它只关注路径名(pathname)。
import { match } from 'path-to-regexp';
// 无法解析 ?q=1
const matchFn = match('/search');
matchFn('/search?q=1');
// 忽略查询字符串,仅匹配路径
query-string 在此场景下表现最佳。支持数组、对象嵌套及自定义编码。
import queryString from 'query-string';
const parsed = queryString.parse('?tags=js&tags=web');
// parsed.tags === ['js', 'web']
const str = queryString.stringify({ q: 'hello' });
// str === 'q=hello'
uri-template 可以通过模板变量处理查询参数,但语法较复杂(RFC 6570 语法)。
import { UriTemplate } from 'uri-template';
// 使用 RFC 6570 语法处理查询
const template = new UriTemplate('/search{?q,page}');
const url = template.expand({ q: 'hello', page: 1 });
// url === '/search?q=hello&page=1'
url-template 同样支持 RFC 6570 的查询参数语法,适合标准化场景。
import urlTemplate from 'url-template';
const template = urlTemplate.parse('/search{?q,page}');
const url = template.expand({ q: 'hello', page: 1 });
// url === '/search?q=hello&page=1'
当需要根据变量动态构建 URL(例如 API 客户端或文档生成)时,模板引擎至关重要。
path-to-regexp 提供 compile 功能,可以反向生成路径,但语法是私有的,非标准。
import { compile } from 'path-to-regexp';
const toPath = compile('/user/:id');
const url = toPath({ id: 123 });
// url === '/user/123'
query-string 可以拼接查询字符串,但需要手动处理路径部分。
import queryString from 'query-string';
// 需手动拼接路径和查询
const path = '/search';
const query = queryString.stringify({ q: 'hello' });
const url = `${path}?${query}`;
// url === '/search?q=hello'
uri-template 支持完整的 RFC 6570 标准,适合需要标准互操作性的场景。
import { UriTemplate } from 'uri-template';
// 支持复杂的表达式如 {+var} 或 {.ext}
const template = new UriTemplate('/files/{name}{.ext}');
const url = template.expand({ name: 'report', ext: 'pdf' });
// url === '/files/report.pdf'
url-template 同样支持 RFC 6570,且 API 设计更为现代简洁。
import urlTemplate from 'url-template';
const template = urlTemplate.parse('/files/{name}{.ext}');
const url = template.expand({ name: 'report', ext: 'pdf' });
// url === '/files/report.pdf'
在选择底层基础设施库时,维护活跃度是架构决策的关键指标。
path-to-regexp 维护极佳。它是 Express、React Router 等主流框架的依赖,更新频繁,安全性高。
// 生态广泛,TypeScript 类型定义完善
import { match } from 'path-to-regexp';
query-string 维护良好。由知名开发者维护,广泛用于前端生态,API 稳定。
// 社区信任度高
import queryString from 'query-string';
uri-template 维护频率较低。npm 上的 uri-template 包更新较慢,可能存在长期未修复的 Issue。
// 建议在新项目中谨慎评估,考虑替代方案
import { UriTemplate } from 'uri-template';
url-template 维护相对活跃。bramus/url-template 是 RFC 6570 实现中较受推荐的一个,适合替代 uri-template。
// 更推荐的 RFC 6570 实现
import urlTemplate from 'url-template';
| 特性 | path-to-regexp | query-string | uri-template | url-template |
|---|---|---|---|---|
| 核心用途 | 路由匹配与路径编译 | 查询参数解析与序列化 | RFC 6570 模板展开 | RFC 6570 模板展开 |
| 路径参数提取 | ✅ 强大 (支持正则) | ❌ 不支持 | ⚠️ 非主要功能 | ⚠️ 非主要功能 |
| 查询参数处理 | ❌ 不支持 | ✅ 专业 (支持数组) | ✅ 通过模板语法 | ✅ 通过模板语法 |
| URL 生成 | ✅ 路径编译 | ⚠️ 仅查询部分 | ✅ 完整模板展开 | ✅ 完整模板展开 |
| 标准遵循 | 私有语法 | 事实标准 | RFC 6570 | RFC 6570 |
| 维护状态 | 🟢 非常活跃 | 🟢 活跃 | 🟡 较低 | 🟢 较好 |
组合使用是最佳实践。
path-to-regexp 处理服务端或前端路由匹配。它是行业标准,性能最优。query-string 处理浏览器地址栏的查询参数。它的 API 最符合 JavaScript 开发习惯。url-template 进行 URL 生成。它比 uri-template 包更可靠。避免重复造轮子。不要试图用 query-string 去匹配路径,也不要用 path-to-regexp 去解析复杂的查询数组。让每个工具做它最擅长的事,代码会更清晰、更易维护。
如果你只需要处理 URL 中的查询参数部分(即 ? 之后的内容),query-string 是最轻量且功能完备的选择。它擅长处理数组、编码和解码,适合在浏览器端或 Node.js 中快速解析 location.search。
如果你的核心需求是路由匹配(例如在 Node.js 服务器或自定义前端路由器中解析 URL 路径参数),path-to-regexp 是首选。它是 Express 和 React Router 的底层依赖,社区支持最强,适合处理 /users/:id 这种路径模式。
如果你需要严格遵循 RFC 6570 标准 进行 URL 模板展开,且项目依赖较旧的生态,可以考虑 uri-template。但需注意其维护频率较低,若在新项目中需要 RFC 6570 支持,建议优先评估更活跃的替代方案。
如果你需要 RFC 6570 URL 模板展开 功能(例如构建 HATEOAS API 客户端或动态生成复杂 URL),url-template(by Bramus)是更现代、维护更活跃的选择。它比 uri-template 包更受推荐,适合需要标准化模板语法的场景。
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.