nuqs

@beignet/nuqs connects contract query schemas to URL-backed state. Use it for search, filters, tabs, sorting, and pagination when the URL should reflect the current view.

bun add @beignet/nuqs @beignet/react-query @tanstack/react-query nuqs

Setup

import { createNuqs } from "@beignet/nuqs";

export const nq = createNuqs();

Bind the contract builder directly with nq(contract). The helper reads the contract query schema and keeps URL state aligned with the same input shape used by the client.

In Next.js App Router, mount the NuqsAdapter once:

import { NuqsAdapter } from "@beignet/nuqs/next/app";

export function Providers({ children }: { children: React.ReactNode }) {
  return <NuqsAdapter>{children}</NuqsAdapter>;
}

URL-backed filters

import { useQuery } from "@tanstack/react-query";
import { parseAsString, parseAsStringLiteral } from "nuqs";
import { listContacts } from "@/features/contacts/contracts";
import { nq } from "@/lib/nq";
import { rq } from "@/client/rq";

const contactsSearch = nq(listContacts).query({
  parsers: {
    search: parseAsString,
    group: parseAsStringLiteral(["personal", "work", "family", "other"]),
  },
  history: "replace",
});

function ContactsPage() {
  const [filters, setFilters] = contactsSearch.useState();

  const query = useQuery(
    contactsSearch.toQueryOptions(rq(listContacts), filters, {
      query: { limit: 50, offset: 0 },
    }),
  );

  return null;
}

toQueryOptions(...) composes with @beignet/react-query, so URL state and query input stay aligned with the same contract.