paramour
Guides

nuqs Adapter

Derive nuqs parsers from a route's search codecs.

nuqs is the standard tool for writing search-param state from client components — useQueryState(s) with batched updates, history control, and shallow routing. What it asks of you is a parser per key. @paramour-js/nuqs derives those parsers from the codecs your route already declares, so URL state is never defined twice.

npm install @paramour-js/nuqs nuqs

A filter panel from a route

nuqsParsers(route) turns a route's whole search config into an ordinary nuqs parser map:

app/products/filter-panel.tsx
// @filename: app/products/route.def.ts
import { ,  } from "paramour";

export const  = ("/products", {
  : {
    : .().(1),
    : .().(),
    : .(),
  },
});

// @filename: app/products/filter-panel.tsx
"use client";

import {  } from "@paramour-js/nuqs";
import {  } from "nuqs";

import {  } from "./route.def";

export function () {
  const [{ , ,  }, ] = (
    (),
  );

  return (
    <>
      <
        ={() => void ({ : 1, : .. })}
        ={ ?? ""}
      />
      < ={() => void ({ :  + 1 })}>
        next page ({.} tags active)
      </>
    </>
  );
}

Everything the codecs declared carries over: page reads non-nullable (its .default(1) became withDefault), q reads string | null (nuqs's spelling of absent), tags is a repeated-key multi-parser that reads [] when absent, and every value round-trips through the same wire format your server parse expects. When the route's config changes, this component's types change with it.

The output is ordinary nuqs currency — withOptions, createSerializer, createLoader, and the server cache all compose untouched. The adapter imports from nuqs/server internally, so deriving parsers is safe in server code too.

Single-codec derivation

nuqsParser(codec) is the one-key form, useful when a component owns a single param:

// @filename: app/products/route.def.ts
import { ,  } from "paramour";

export const  = ("/products", {
  : { : .().(1), : .().() },
});

// @filename: app/products/search-box.tsx
"use client";

import {  } from "@paramour-js/nuqs";
import {  } from "nuqs";
import {  } from "paramour";

export function () {
  const [, ] = ("q", (.().()));

  return < ={() => void (..)} ={ ?? ""} />;
}

The derivation rules

The adapter reads presence and defaults off the codec so the nuqs reading matches core's decode:

Codec shapenuqs parser
required or .optional()nullable read (null = absent)
.default(value)withDefault(value) — non-nullable read
.default(() => value) (factory)nullable — a frozen default would lie
.catch(fallback)malformed reads recover to the fallback
p.array(…)multi (repeated-key) parser, [] when absent

What refuses to compile

nuqs reserves null to mean "absent or unparseable" — so a codec whose legitimate output includes null has no faithful nuqs twin, and the adapter rejects it at the call site with the reason in the error, rather than silently conflating your null with nuqs's:

import {  } from "@paramour-js/nuqs";
import {  } from "paramour";

const  = .<null | string>({
  : () => ( === "null" ? null : ),
  : () =>  ?? "null",
});

(maybe);
Argument of type 'Codec<string | null, "required", false, "single", boolean>' is not assignable to parameter of type 'Codec<string | null, "required", false, "single", boolean> & NoNuqsTwin<"codec output includes null, which nuqs reserves for absent/unparseable">'. Property '[noTwinReason]' is missing in type 'Codec<string | null, "required", false, "single", boolean>' but required in type 'NoNuqsTwin<"codec output includes null, which nuqs reserves for absent/unparseable">'.

rawSearch routes are rejected the same way (there are no per-key codecs to derive from), as are search-less routes. Plain-JavaScript callers get the same judgments as loud runtime errors.

Server and client, one contract

The point of deriving rather than declaring: your server components parse with the route, your client components set state through parsers derived from the route, and standardSearchSchema exports the same contract to tRPC and friends. One search config, every consumer.

On this page