Handling Query Parameters Correctly with URLSearchParams

The basics of URLSearchParams and how to preserve repeated query parameters when converting from an object.

Published
September 30, 2023
日本語で読む

new URLSearchParams({ q: ["greeting", "foobar"] }).toString() returns q=greeting%2Cfoobar. The array was implicitly converted to a string, and two values became one comma-separated value. Write a filter into the URL without noticing, and the server receives a single entry.

This covers the basics of URLSearchParams and how to keep repeated keys intact. The Next.js App Router exposes a read-only interface through useSearchParams as well.

Creating URLSearchParams

You can construct URLSearchParams from a query string, a record, or an iterable of key-value pairs.

const empty = new URLSearchParams();

const fromString = new URLSearchParams("q=greeting");
const fromRecord = new URLSearchParams({ q: "greeting" });
const fromEntries = new URLSearchParams([["q", "greeting"]]);

An iterable such as Map works as well.

const map = new Map();
map.set("q", "greeting");

const params = new URLSearchParams(map);

toString() serializes the parameters without the leading question mark.

const params = new URLSearchParams("q=greeting");

params.toString();
// 'q=greeting'

Repeated keys are serialized as q=greeting&q=foobar. If a server specifically expects q[]=greeting&q[]=foobar, use q[] as the key.

const params = new URLSearchParams([
  ["q", "greeting"],
  ["q", "foobar"],
]);

params.toString();
// 'q=greeting&q=foobar'

Use append, delete, and set to modify values. get returns the first value for a key, while getAll returns every value. You can iterate with entries and sort by key with sort.

Converting an object with repeated keys

In the Next.js Pages Router, router.query can represent repeated keys as arrays.

{
  q: ["greeting", "foobar"];
}

Passing that object directly produces the comma-separated string from the opening example.

const params = new URLSearchParams({ q: ["greeting", "foobar"] });

params.toString();
// 'q=greeting%2Cfoobar'

Convert the object to a list of string pairs instead.

const query = {
  q: ["greeting", "foobar"],
  r: "routing",
};

const entries: [string, string][] = [];

for (const [key, value] of Object.entries(query)) {
  if (typeof value === "string") {
    entries.push([key, value]);
  } else {
    value.forEach((item) => entries.push([key, item]));
  }
}

const params = new URLSearchParams(entries);

When you already have the full URL, use the searchParams property of a URL instance.

const url = new URL("https://example.com?q=greeting&q=foobar&r=routing#ddd");

url.searchParams.toString();
// 'q=greeting&q=foobar&r=routing'

Closing note

The platform already provides capable URL APIs: URLSearchParams, URL, and URLPattern. Checking whether one of them fits before joining or splitting strings by hand removes a whole class of encoding and repeated-value bugs.

Reference