· By John Kavanagh

Converting Objects to Query Strings and Vice Versa

Abstract image used to represent Converting Objects to Query Strings and Vice Versa
Image by David Clode.

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 keyvalue pairs matters, including pairs interleaved between different keys.

Query strings look like simple keyvalue 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 keyvalue 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=2

In this URL, query=JavaScript and page=2 are parameters that can be read and used by the server or clientside application. Query strings are commonly used in APIs, search functionality, and clientside 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 builtin 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 URLbased 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, stringarray and temporarily undefined values, while a custom hook can update the URL without a full page reload.

Key Takeaways

  • Query strings enable passing keyvalue data via URLs.
  • The URLSearchParams API 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.query and modelled as string, string array, or undefined.
  • 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.


Want to find out more?

If you need senior handson support with a complex React or Next.js platform, migration, performance issue, or technical SEO problem, send me the context and I'll tell you where I can help.