Converting Objects to Query Strings and Vice Versa

In Brief
Use URLSearchParams to encode and parse ordinary query strings, then decide how your application represents repeated keys, arrays, empty values, and reconstructed types. The object helpers below preserve the order of values within each key. Keep URLSearchParams or its entries when the order of all key‑value pairs matters, including pairs interleaved between different keys.
Query strings look like simple key‑value data until an application needs repeated keys, arrays, empty values, or state that must survive a reload. Moving between a JavaScript object and a URL therefore needs an explicit convention, especially in Next.js where router values can also be temporarily undefined.
What is a Query String?
Starting with the absolute basics, a query string is a part of a URL which contains key‑value pairs used to pass data between pages or to an API. It begins with a ? and separates parameters with &. For example:
https://example.com/search?query=JavaScript&page=2In this URL, query=JavaScript and page=2 are parameters that can be read and used by the server or client‑side application. Query strings are commonly used in APIs, search functionality, and client‑side state management.
Converting an Object to a Query String
JavaScript provides several ways to construct query strings from objects, depending on the level of control needed.
Using URLSearchParams
The URLSearchParams API (which has been available in browsers for eleven years now) simplifies working with query strings:
const params = { search: "JavaScript", page: "2", sort: "asc" };
const queryString = new URLSearchParams(params).toString();
console.log(queryString); // Output: "search=JavaScript&page=2&sort=asc"This method automatically encodes special characters and produces a properly formatted query string. The record passed to the constructor contains strings; use append() when a key has more than one value.
Custom Function for More Control
If you need more flexibility, you could implement a custom function like this:
type QueryValue = string | number | boolean;
type QueryObject = Record<string, QueryValue | QueryValue[]>;
const objectToQueryString = (obj: QueryObject): string => {
const query = new URLSearchParams();
Object.entries(obj).forEach(([key, value]) => {
const values = Array.isArray(value) ? value : [value];
values.forEach((item) => {
query.append(key, String(item));
});
});
return query.toString();
};
const params = { search: "JavaScript", page: 2, sort: "asc" };
console.log(objectToQueryString(params));
// Output: "search=JavaScript&page=2&sort=asc"Here, the function converts an object into a query string and uses append() for every value. Scalars produce one pair, whilst arrays produce repeated keys in their existing array order. Special characters are encoded by URLSearchParams. Nested objects are outside this helper's input contract and would need their own agreed representation.
Parsing a Query String into an Object
To extract parameters from a query string and convert them into an object, JavaScript also provides built‑in utilities.
Using URLSearchParams
The simplest way to parse query strings is to use URLSearchParams again, like this:
const queryString = "search=JavaScript&page=2&sort=asc&tag=css&tag=javascript";
const searchParams = new URLSearchParams(queryString);
const params: Record<string, string | string[]> = Object.create(null);
searchParams.forEach((value, key) => {
const current = params[key];
if (current === undefined) {
params[key] = value;
} else {
params[key] = Array.isArray(current) ? [...current, value] : [current, value];
}
});
console.log(params);
// Output: { search: "JavaScript", page: "2", sort: "asc", tag: ["css", "javascript"] }The values are strings. In this example, repeated keys are represented as string arrays rather than silently retaining only the final value. Convert values to other types only when the application's input contract requires it. The result uses Object.create(null), so keys such as __proto__, constructor, and toString are ordinary own properties rather than inherited behaviour. It also has no inherited object methods.
Custom Function for Parsing
A small wrapper can define how repeated values are represented without reimplementing URL parsing and decoding:
type QueryObject = Record<string, string | string[]>;
const queryStringToObject = (query: string): QueryObject => {
const searchParams = new URLSearchParams(query.startsWith("?") ? query.slice(1) : query);
const result: QueryObject = Object.create(null);
searchParams.forEach((value, key) => {
const current = result[key];
if (current === undefined) {
result[key] = value;
} else {
result[key] = Array.isArray(current) ? [...current, value] : [current, value];
}
});
return result;
};
console.log(queryStringToObject("?search=JavaScript&page=2&tag=css&tag=javascript"));
// Output: { search: "JavaScript", page: "2", tag: ["css", "javascript"] }This keeps parsing and decoding with URLSearchParams, whilst the wrapper represents repeated keys as arrays. It preserves the order within each array, but grouping by key loses the original interleaving: a=1&b=2&a=3 becomes one a array and one b value. Keep the URLSearchParams object or use Array.from(searchParams.entries()) when the complete pair order matters.
Handling Query Strings in the Next.js Pages Router
In the Next.js Pages Router, query strings are commonly used for search parameters and URL‑based state. The router exposes utilities for reading and updating them.
Accessing Query Parameters in Next.js
In the Pages Router, Next.js provides the useRouter hook, which you can use like this:
import { useRouter } from "next/router";
type QueryValue = string | string[] | undefined;
const firstQueryValue = (value: QueryValue) => {
return Array.isArray(value) ? value[0] : value;
};
const SearchPage = () => {
const router = useRouter();
const search = firstQueryValue(router.query.search);
const page = firstQueryValue(router.query.page);
const sort = firstQueryValue(router.query.sort);
return (
<div>
<h1>Search Results</h1>
<p>Search Term: {search}</p>
<p>Page: {page}</p>
<p>Sort Order: {sort}</p>
</div>
);
};
export default SearchPage;Query parameters are available in router.query as an object, but ‑ as with using URLSearchParams ‑ each value can be a string, a string array, or undefined until the router is ready. The example selects the first value explicitly because the page expects one value per field.
Updating Query Parameters in Next.js
To update the URL with new query parameters without reloading the page, we can define a custom hook around router.push:
import { useRouter } from "next/router";
type QueryValue = string | string[] | undefined;
type QueryUpdates = Record<string, QueryValue>;
const useUpdateQuery = () => {
const router = useRouter();
return (newParams: QueryUpdates) => {
void router.push(
{
pathname: router.pathname,
query: { ...router.query, ...newParams },
},
undefined,
{ shallow: true },
);
};
};This allows us to modify the query string dynamically without causing a full page refresh.
Wrapping Up
Converting objects to query strings and parsing query strings into objects is a key technique in JavaScript, especially when working with URLs. The URLSearchParams API provides a simple way to handle this, but custom functions allow for greater flexibility.
In a Pages Router component, useRouter exposes string, string‑array and temporarily undefined values, while a custom hook can update the URL without a full page reload.
Key Takeaways
- Query strings enable passing key‑value data via URLs.
- The
URLSearchParamsAPI simplifies converting objects to query strings and vice versa. - Custom functions provide more control, such as handling arrays and complex structures.
- Query parameters in Next.js are accessed via
router.queryand modelled as string, string array, orundefined. - Updating query parameters in Next.js can be done using
router.push()with shallow routing.
URLSearchParams is the clearest starting point for ordinary string keys and values. Repeated keys, arrays, nested objects, nulls, and booleans need an explicit application convention because a query string does not preserve those JavaScript types by itself. Define that contract before writing the conversion helper.