URLSearchParams
new URLSearchParams() URLSearchParams is a built-in JavaScript object that provides a consistent interface for reading and writing the query string portion of a URL. It landed in browsers with Chrome 51, Firefox 49, and Safari 10.1, and has been available in Node.js since version 10. The interface follows the WHATWG URL spec, which means behavior is consistent across environments — unlike the older querystring module that Node.js used to rely on.
You rarely construct URLSearchParams in isolation. The more common entry point is reading url.searchParams from a URL object, which gives you a live view of the URL’s query string. Changes you make through the URLSearchParams API write back through to the URL’s search property.
Syntax
new URLSearchParams()
new URLSearchParams("foo=bar&baz=qux")
new URLSearchParams({ foo: "bar", baz: "qux" })
new URLSearchParams([["foo", "bar"], ["baz", "qux"]])
new URLSearchParams(existingSearchParams)
The constructor accepts a wide range of input types. A string is parsed as a query string (decoding happens automatically). An object treats its enumerable string properties as key/value pairs. An array of two-element arrays works the same way. You can also pass another URLSearchParams instance to get a copy.
Constructor
When called with no arguments, you get an empty URLSearchParams object ready to be populated:
const params = new URLSearchParams();
params.append("page", "1");
params.toString();
// "page=1"
Passing a string is the most common constructor form. The string is parsed as a query string, and percent-encoded sequences like %26 are automatically decoded into their original characters. This saves you from having to call decodeURIComponent separately on every value you extract.
const params = new URLSearchParams("name=Alice%26Bob&age=30");
params.get("name"); // "Alice&Bob" — decoded
params.get("age"); // "30"
Instance Methods
URLSearchParams has eleven instance methods that cover the full range of read, write, and iteration operations. These methods follow a consistent naming pattern — get, set, has, delete — that mirrors other JavaScript collections like Map and FormData.
Reading values
get(name) returns the first value for a key, or null if the key is absent:
const params = new URLSearchParams("q=hello&q=world&q!");
params.get("q"); // "hello"
params.get("page"); // null
When a key appears multiple times in the query string, get() only hands you the first occurrence. To collect every value under a given key, you need getAll(), which returns all values as an array regardless of how many times the key repeats.
getAll(name) returns every value for a key as an array:
params.getAll("q"); // ["hello", "world", "!"]
Beyond getting and listing values, you often just need to know whether a parameter is present at all. has() answers that question without requiring you to pull the value, and it also accepts an optional second argument to check for a specific value match.
has(name) tells you whether a key exists, and optionally checks for a specific value:
params.has("q"); // true
params.has("q", "hello"); // true
params.has("q", "world"); // true
params.has("other"); // false
The reading methods give you a complete picture of what the query string contains. The writing methods let you build and modify that picture.
Writing values
append(name, value) adds a new key/value pair. If the key already exists, it does not remove the existing entries — it adds another one:
const params = new URLSearchParams();
params.append("tag", "javascript");
params.append("tag", "es6");
params.getAll("tag"); // ["javascript", "es6"]
append() is additive — it never removes existing values. If you need to replace all values for a key, use set() instead. This distinction mirrors the difference between adding another HTTP header with the same name and overwriting one completely.
set(name, value) always replaces all existing values for that key:
const params = new URLSearchParams("tag=javascript&tag=es6");
params.set("tag", "ts");
params.getAll("tag"); // ["ts"]
delete() is the only method that accepts an optional value argument. Passing only a name wipes every entry for that key. Passing both name and value removes only the specific key-value pair, leaving other entries under the same name intact.
delete(name, value?) removes entries. With only a name, it removes every entry for that key. With a value argument, it only removes entries that match both the name and value:
const params = new URLSearchParams("tag=js&tag=ts&tag=rust");
params.delete("tag", "ts");
// remaining: tag=js, tag=rust
params.delete("tag");
// all gone
Query strings are not inherently ordered in the URL spec, and the order of parameters usually does not affect server behavior. However, sort() is useful when you are building a cache key from a query string or when you want deterministic output for testing or comparison.
sort() orders all keys in ascending code point order. Unlike a plain Map, URLSearchParams preserves insertion order — sort() is the only way to reorder:
const params = new URLSearchParams("z=1&a=2&m=3");
params.sort();
params.toString(); // "a=2&m=3&z=1"
toString() produces a query string ready for use in a URL. Spaces become +, and special characters are percent-encoded according to the query string format, not the general URI format. This encoding differs from what encodeURIComponent() produces, which is why you should always let URLSearchParams handle the encoding rather than pre-encoding values yourself.
toString() returns the query string in percent-encoded form, suitable for assigning to URL.search:
const params = new URLSearchParams();
params.append("name", "Alice & Bob");
params.append("q", "hello world");
params.toString();
// "name=Alice%20%26%20Bob&q=hello+world"
The iteration methods make URLSearchParams feel like a Map-like data structure. You can use for...of with keys(), values(), and entries(), or pass the instance directly to Array.from().
Iteration
keys(), values(), and entries() each return an iterator. Combined with for...of, this gives you a clean way to walk the params:
const params = new URLSearchParams("color=red&size=large&color=blue");
for (const key of params.keys()) {
console.log(key);
}
// color
// size
// color
for (const [key, value] of params.entries()) {
console.log(`${key} = ${value}`);
}
// color = red
// size = large
// color = blue
If you prefer a callback style over iterators, forEach() works the same way — with one important difference from Map: the callback receives (value, key) in that order, matching the callback signature that forEach uses elsewhere in the platform.
forEach(callback, thisArg?) iterates with the same data, passing (value, key, searchParams) to the callback:
params.forEach((value, key) => {
console.log(`${key}: ${value}`);
});
The size property gives you a count of all key-value entries without iterating through them. This is especially useful when you want to check whether a query string is empty before processing it further.
The size property
The size property gives you the total number of key/value entries without needing to count manually:
const params = new URLSearchParams("a=1&b=2&c=3");
params.size; // 3
Supported in Node.js 16+, Chrome 101+, Firefox 128+, and Safari 16.4+. For older environments, fall back to params.keys().length.
Common Gotchas
Double encoding
If you pre-encode a value and then pass it to append() or set(), URLSearchParams will encode it again. A space becomes %2520 instead of %20. Always pass raw strings and let URLSearchParams handle encoding:
// wrong — double-encoded
params.append("q", encodeURIComponent("hello world"));
params.toString(); // "q=hello%2520world"
// correct — raw string
params.append("q", "hello world");
params.toString(); // "q=hello%20world"
Plus sign as space
In query string format, + represents a space. So parsing the string "bin=E+AXQB+A" will give you "E AXQB A" when you call get("bin"). This trips people up when working with Base64 data or unencoded + characters in values. Encode first if you need the + preserved:
const params = new URLSearchParams("bin=E+AXQB+A");
params.get("bin"); // "E AXQB A" — plus decoded to space
const params2 = new URLSearchParams();
params2.append("bin", encodeURIComponent("E+AXQB+A"));
params2.get("bin"); // "E%2BAXQB%2BA"
Decoding asymmetry between get() and toString()
get() returns a decoded value, but toString() returns a re-encoded string. This means round-tripping through toString() does not give you back the original stored representation — values are normalized on both read and write.
Iteration order is preserved
Unlike Map, URLSearchParams preserves the order in which keys were added (or as they appeared in the original query string). This matters when the order of parameters affects an API’s behavior — many REST APIs treat ?a=1&b=2 and ?b=2&a=1 as different requests.
Using with the URL interface
The URL interface exposes searchParams as a live URLSearchParams object:
const url = new URL("https://example.com/search?q=hello&page=2");
url.searchParams.get("q"); // "hello"
url.searchParams.set("page", "3");
url.search; // "?q=hello&page=3"
Modifying url.searchParams updates url.search and vice versa. This two-way binding is the most common way URLSearchParams is used in practice.