← Back to articles
Ecosystem nuqsNext.jsReact

nuqs: Type-Safe URL Search Params for Next.js

· 4 min read

URL search params are the most underused state management tool in React applications. They survive page refreshes, they’re shareable via links, and they’re indexed by search engines. But working with URLSearchParams directly is painful: everything is a string, there’s no validation, and synchronizing with React state requires boilerplate. nuqs makes search params first-class citizens in Next.js with type-safe parsers, reactive hooks, and server component support.

Typed search params with parsers

nuqs provides built-in parsers that convert URL strings to typed values. No more manual parseInt or === 'true' checks scattered across components.

import { parseAsString, parseAsInteger, parseAsStringEnum, useQueryStates } from 'nuqs';

const statusOptions = ['all', 'active', 'archived'] as const;

export default function ProjectsPage() {
  const [filters, setFilters] = useQueryStates({
    search: parseAsString.withDefault(''),
    page: parseAsInteger.withDefault(1),
    status: parseAsStringEnum(statusOptions).withDefault('all'),
  });

  return (
    <div>
      <input
        value={filters.search}
        onChange={(e) => setFilters({ search: e.target.value })}
        placeholder="Search projects..."
      />
      <select
        value={filters.status}
        onChange={(e) => setFilters({ status: e.target.value, page: 1 })}
      >
        {statusOptions.map((s) => (
          <option key={s} value={s}>
            {s}
          </option>
        ))}
      </select>
    </div>
  );
}

useQueryStates groups multiple params into a single state object. Updating one param can reset another in the same call, like resetting page to 1 when changing a filter. The URL stays in sync automatically, and every value is typed correctly.

Server component support

nuqs works in server components through its createSearchParamsCache utility. This lets you read and validate search params at the server level, avoiding client-side hydration mismatches and enabling data fetching based on URL state.

import { createSearchParamsCache, parseAsString, parseAsInteger } from 'nuqs/server';

export const searchParamsCache = createSearchParamsCache({
  search: parseAsString.withDefault(''),
  page: parseAsInteger.withDefault(1),
});
import { searchParamsCache } from '@/lib/search-params';

export default async function ProjectsPage({
  searchParams,
}: {
  searchParams: Promise<Record<string, string | string[]>>;
}) {
  const { search, page } = searchParamsCache.parse(await searchParams);

  const projects = await db.project.findMany({
    where: search ? { name: { contains: search } } : undefined,
    skip: (page - 1) * 20,
    take: 20,
  });

  return <ProjectList projects={projects} />;
}

The server reads typed params directly from the URL without client JavaScript. The database query uses validated, typed values. No parseInt(searchParams.page || '1') gymnastics.

Shallow updates without re-renders

By default, nuqs updates the URL using history.pushState without triggering a Next.js navigation. This means changing a search param doesn’t re-run server components or cause a full page transition. The URL updates, the client state updates, and nothing else happens.

import { useQueryState, parseAsString } from 'nuqs';

export function SearchInput() {
  const [search, setSearch] = useQueryState(
    'search',
    parseAsString.withDefault('').withOptions({ shallow: true, throttleMs: 300 })
  );

  return (
    <input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Filter..." />
  );
}

The throttleMs option debounces URL updates so typing fast doesn’t flood the browser history. Combined with shallow: true, this gives a search input that updates the URL in real time without any server round-trips or layout shifts.

Custom parsers for complex state

Beyond the built-in parsers, nuqs supports custom serializers for any data structure. Date ranges, arrays, and nested objects can all live in the URL with proper type safety.

import { createParser } from 'nuqs';

const parseAsDateRange = createParser({
  parse: (value: string) => {
    const [from, to] = value.split(',');
    if (!from || !to) return null;
    return { from: new Date(from), to: new Date(to) };
  },
  serialize: ({ from, to }) =>
    `${from.toISOString().split('T')[0]},${to.toISOString().split('T')[0]}`,
});

The URL becomes ?range=2024-01-01,2024-03-31 instead of managing two separate params. The parser validates the input and returns null for malformed values, which falls back to the default.

Conclusion

nuqs turns URL search params from a string manipulation chore into a proper state management layer. Type-safe parsers prevent runtime errors, server component support enables SSR-friendly filtering, and shallow updates keep the UI responsive. For any Next.js application with filters, pagination, or sortable tables, it’s the cleanest way to keep state in the URL where it belongs.