How to convert a JavaScript object to a query string (and back)
Turn a JavaScript object into URL query parameters with URLSearchParams, handle arrays, null, dates and nested objects correctly, and parse a query string back into an object.
Updated 4 min read

In this article
The quickest way to convert a JavaScript object to a query string is new URLSearchParams(object).toString(). It is built into every browser and Node.js and encodes values correctly. It does have gaps: null and undefined become the text "null" and "undefined", arrays become comma-joined text, and nested objects become [object Object]. This guide shows the one-liner, a small tested helper that fixes those gaps, a version for nested objects, and how to read a query string back into an object.
Tested with Node.js 24.19.0 (V8 13.6). Every result shown in a comment was printed by running the code; the same code runs in Deno, Bun and current Chrome, Firefox and Safari.
The one-liner: URLSearchParams
const params = new URLSearchParams({ q: 'red shoes', page: 2, sort: 'price&asc' });
console.log(params.toString()); // q=red+shoes&page=2&sort=price%26asc
const url = new URL('https://shop.example/search');
url.search = params;
console.log(url.href); // https://shop.example/search?q=red+shoes&page=2&sort=price%26asc
// Values are converted with String(), which is rarely what you want for these:
console.log(new URLSearchParams({ a: null, b: undefined, c: [1, 2], d: { x: 1 } }).toString());
// a=null&b=undefined&c=1%2C2&d=%5Bobject+Object%5DTwo details in that output matter. URLSearchParams encodes a space as +, which servers read as a space in a query string, and it encodes & and = inside values so they cannot break the string apart. Assigning it to url.search on a URL object is the safest way to build a full address, because the base URL is parsed and validated too.
A helper for arrays, null, undefined and dates
Most real objects, such as a form's filter state, contain empty values and lists. This helper leaves out null and undefined, repeats the key for each array item (the format most servers expect) and sends dates as ISO 8601. It refuses nested objects rather than sending [object Object]:
// Skips null and undefined, repeats the key for arrays (tag=a&tag=b), and turns dates into ISO strings.
export function toQueryString(values) {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(values)) {
for (const item of Array.isArray(value) ? value : [value]) {
if (item === null || item === undefined) continue;
if (typeof item === 'object' && !(item instanceof Date)) throw new TypeError(`Nested value for "${key}": flatten it first`);
params.append(key, item instanceof Date ? item.toISOString() : String(item));
}
}
return params.toString();
}toQueryString({
q: 'café au lait', tag: ['php', 'js'], page: 1, draft: false, author: null,
from: new Date('2026-10-01T00:00:00Z'),
});
// q=caf%C3%A9+au+lait&tag=php&tag=js&page=1&draft=false&from=2026-10-01T00%3A00%3A00.000Zfalse and 0 are kept, because they are real values; only missing values are dropped. If your API expects tag=php,js or tag[]=php instead of a repeated key, change the inner loop; check the server's documentation, because there is no single standard for arrays in query strings.
Nested objects: bracket notation
PHP, Ruby on Rails and the popular qs package read filter[price][max]=50 as a nested object. To send that format, recurse and build the bracketed names:
// Bracket notation (filter[price][max]=50), the format PHP, Rails and the qs package read as nested objects.
export function toBracketQuery(values, prefix = '', params = new URLSearchParams()) {
for (const [key, value] of Object.entries(values)) {
const name = prefix ? `${prefix}[${key}]` : key;
if (value === null || value === undefined) continue;
if (Array.isArray(value)) value.forEach((item) => params.append(`${name}[]`, String(item)));
else if (typeof value === 'object' && !(value instanceof Date)) toBracketQuery(value, name, params);
else params.append(name, value instanceof Date ? value.toISOString() : String(value));
}
return params;
}toBracketQuery({ filter: { price: { max: 50 }, brand: ['acme', 'zen'] }, q: 'shoes' }).toString();
// filter%5Bprice%5D%5Bmax%5D=50&filter%5Bbrand%5D%5B%5D=acme&filter%5Bbrand%5D%5B%5D=zen&q=shoes
// decoded: filter[price][max]=50&filter[brand][]=acme&filter[brand][]=zen&q=shoesWe checked that PHP's parse_str reads that string back as {"filter":{"price":{"max":"50"},"brand":["acme","zen"]}}. For deeply nested or frequently changing structures, sending JSON in a POST body is usually simpler than a long query string.
Parsing a query string back into an object
const params = new URLSearchParams('tag=php&tag=js&page=2&q=red+shoes');
params.get('q'); // 'red shoes'
params.get('page'); // '2' (always a string: convert it yourself)
params.getAll('tag'); // [ 'php', 'js' ]
Object.fromEntries(params); // { tag: 'js', page: '2', q: 'red shoes' } (the first tag is lost!)
// In the browser, for the current page:
const current = new URLSearchParams(window.location.search);Object.fromEntries keeps only the last value for a repeated key, so use getAll() for any parameter that can repeat. Every value comes back as a string, so Number(params.get('page')) and similar conversions, with validation, are your job.
encodeURIComponent, querystring and jQuery
encodeURIComponent('red shoes & socks'); // 'red%20shoes%20%26%20socks'
new URLSearchParams({ x: 'red shoes & socks' }).toString(); // 'x=red+shoes+%26+socks'
const manual = (obj) => Object.entries(obj)
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&');
manual({ q: 'red shoes', sort: 'price&asc' }); // 'q=red%20shoes&sort=price%26asc'encodeURIComponentencodes a space as%20. Both forms are valid in a query string; use it when you build one value at a time, neverencodeURI, which leaves&and=unencoded.- Node's
querystringmodule still works (querystring.stringify({ q: 'red shoes', tag: ['a', 'b'] })givesq=red%20shoes&tag=a&tag=b), but the Node.js documentation marks it as legacy. UseURLSearchParams, which is global in Node.js. - jQuery's
$.param()produces bracket notation for nested data. There is no reason to load jQuery for this in new code.
Security notes
Query strings end up in server logs, browser history and the Referer header, so never put passwords, tokens or personal data in them. Treat parsed values as untrusted input: validate them on the server, and never insert them into HTML without escaping. To cancel a request built from these parameters when the user changes a filter, see cancelling fetch requests with AbortController.
Frequently asked questions
How do I convert an object to a query string in JavaScript?
Use
new URLSearchParams(object).toString(). For objects withnullvalues, arrays or dates, build the parameters withappend()in a loop so that empty values are skipped and arrays become repeated keys.Why does URLSearchParams encode spaces as plus signs?
It follows the
application/x-www-form-urlencodedformat, in which+means a space. Servers decode it as a space in query strings.encodeURIComponentuses%20; both are valid.How do I add an array to a query string?
Call
params.append('tag', value)once per item to gettag=a&tag=b, and read it back withparams.getAll('tag'). Some servers expecttag[]=aortag=a,binstead, so check what yours reads.Can URLSearchParams handle nested objects?
No. Nested values are converted to the text
[object Object]. Build bracket-notation names such asfilter[price][max]yourself, or send nested data as JSON in a request body.
Your next customer could start right here.
Introduce your tool, product or business to readers exploring technology.
Explore placements and request a private quote
Sources
- URLSearchParams — MDN Web Docs
- URL: search property — MDN Web Docs
- encodeURIComponent() — MDN Web Docs
- URL Standard: application/x-www-form-urlencoded — WHATWG
- Query string (legacy module) — Node.js
- parse_str — PHP Manual
Written with substantial AI assistance. Facts checked against cited sources. Editorial policy.
Comments
No comments yet. Be the first to join the conversation.
Leave a comment
Every comment is read by a moderator before it appears. Your email address is never published; it is used only to contact you about this comment.
More from Cloudsoft Zone

Industry updateJavaScript
tsrs explained: can a Rust port make TypeScript checks 8x faster?
tsrs is an experimental Rust port of the TypeScript compiler from Cloudflare and VoidZero. What the 8x speed and 70% memory claims mean, and how to test it.
By Cloudsoft Zone

TutorialDevOps
Laravel backup restore: recover the application, database and uploaded files together
Plan Laravel backup restore around a recoverable set of code, database, uploaded objects and protected keys, then verify the complete application in isolation.
By Cloudsoft Zone

TutorialDevOps
Laravel S3 compatible uploads: private files, MinIO endpoints and safe delivery
Build Laravel S3 compatible uploads with a private disk, scoped credentials, validated files, unique object keys and authorized downloads across MinIO or S3.
By Cloudsoft Zone
