paramour
Reference

@paramour-js/nuqs

The nuqs adapter API.

@paramour-js/nuqs is a thin adapter that derives nuqs parsers from the codecs a route already declares, so URL state has a single source of truth: presence, defaults, catch recovery, and equality are all read off the codec, never declared a second time. The output is ordinary nuqs currency — useQueryState(s), withOptions, createSerializer, createLoader, and the server cache all compose untouched. Internally the adapter imports from nuqs/server, so deriving parsers is safe in server code too.

For the guided walkthrough, see the nuqs guide. This page is the API reference.

nuqsParser

Derives one nuqs parser from one codec.

function nuqsParser<C extends AnyCodec>(codec: C): NuqsParserOf<C>;

Pass the codec exactly as it sits in a route's search config — .optional(), .default(), .catch() already applied. The name mirrors nuqs's parseAs* vocabulary from the call site's perspective: it is the parser for that codec's output.

"use client";

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

export function () {
  const [q, ] = ("q", (.().()));
const q: string | null
return < ={() => void (..)} ={ ?? ""} />; }

The derivation rules, per codec shape:

Codec shapeDerived nuqs parser
required or .optional()single parser, nullable read (null = absent)
.default(value)withDefault(value) — non-nullable read
.default(() => value) (factory)nullable read — no withDefault
.catch(fallback)malformed values recover to the fallback
p.array(…) (arity-many)multi (repeated-key) parser with withDefault([])
p.csv(…)single parser reading E[] | null (one key on the wire)

Gotchas worth knowing:

  • Factory defaults stay nullable on purpose. A factory default is time-varying by declaration; freezing one value into withDefault would lie. Absent reads null — apply the factory at the read site if you want the paramour-decoded shape.
  • .catch() parity comes first. A malformed value recovers exactly as the server decode would; only a codec without catch falls back to nuqs's null. Anything that isn't a paramour ParseError — including a throwing catch factory — propagates loudly.
  • Equality is wire-form equality. The parser's eq compares serialized wire strings — the same judgment paramour's own default elision makes — so nuqs's clearOnDefault and paramour's URL elision agree by construction, for every codec kind including p.custom.
  • Value-form defaults are snapshotted once at derivation time. Mutating a reference-typed default (an array, say) after deriving is unsupported.
  • A default only known at render time goes on the derived parser. When the default depends on the clock or on props (the current week, say), the route can't declare it, so leave the key .optional() and call nuqs's own withDefault on the derived parser inside the component. The derived wire-form eq still applies, so clearOnDefault drops the value from the URL whenever it equals the runtime default — no hand-written null elision in your setters. See the guide.
  • Plain-JavaScript callers passing something that is not a codec get a loud ParamourError at derivation time, mirroring the compile-time judgments below.

nuqsParsers

Derives a whole nuqs parser map from a route object (the common case) or a bare SearchConfig.

// Route form: a parser map for the route's search config.
function nuqsParsers<R extends AnyRoute>(route: R): NuqsParserMap</* R's search config */>;
// Config form.
function nuqsParsers<S extends SearchConfig>(config: S): NuqsParserMap<S>;

Each key gets the same per-codec derivation as nuqsParser; the result is a plain object you hand to useQueryStates, createSerializer, createLoader, or the server cache as-is:

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

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

// @filename: app/products/toolbar.tsx
"use client";

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

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

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

  .page;
page: number
.q;
q: string | null
.tags;
tags: string[]
return ( < ={() => void ({ : . + 1 })}> next page </> ); }

page reads non-nullable (its .default(1) became withDefault), q reads string | null (nuqs's spelling of absent), and tags is a repeated-key multi-parser that reads [] when absent — matching core's decode, where an absent arity-many key and [] are the same wire state.

Gotchas:

  • Routes without per-key codecs are rejected — see NoNuqsTwin below. rawSearch routes validate the whole search object with one schema, so there is nothing per-key to derive from; search-less routes and empty configs have no keys at all. The same judgments are loud ParamourErrors at runtime for plain-JS callers.
  • Route vs. config detection is by value shape: only a route carries a router-kind string on its ~router brand, so a search config with a key literally named "~router" (legal — wire keys are arbitrary strings) still takes the config path.
  • The map is built key-safely (Object.entries → Object.fromEntries), so keys like "__proto__" become ordinary own properties of the result.

nuqsArrayOf

A one-key list codec in nuqs's parseAsArrayOf wire format, for routes whose URLs nuqs already wrote (bookmarks, shared links) or that nuqs code elsewhere still reads.

function nuqsArrayOf<E = string>(element?: Codec<E>): Codec<E[]>;

It is an ordinary paramour codec, so it goes in a search config like any other, takes list-level modifiers (nuqsArrayOf().default([])), and derives an ordinary nuqs parser. Elements are strings unless an unmodified element codec is given, as for p.csv.

p.csv is paramour's own one-key list and is strict by design. nuqsArrayOf copies nuqs's two differences instead:

p.csvnuqsArrayOf (and nuqs)
comma inside an elementSerializeError at link-build timeescaped as the literal text %2C
element that fails to parsethe whole key fails (.catch() recovers it)that element is dropped, the rest kept
empty segment (a,,b)parse failurehanded to the element codec (p.string() keeps "")
import { nuqsArrayOf } from "@paramour-js/nuqs";
import { p } from "paramour";

const search = {
  clients: nuqsArrayOf().default([]),
  types: nuqsArrayOf(p.enum(["client", "internal"])).default([]),
};
// ?clients=Acme%252C%20Inc.,Globex&types=client,bogus
// → { clients: ["Acme, Inc.", "Globex"], types: ["client"] }

nuqs's escaping is lossy in two places, and nuqsArrayOf refuses to write either rather than emit a URL that reads back as a different list: an element whose wire form already contains the text %2C (it would come back with a comma), and a list of one element whose wire form is empty (the empty wire string reads back as []). Both are a SerializeError, thrown from a nuqs setter too: setTags([""]) throws instead of writing ?tags=.

Beyond those two lists, nuqsArrayOf and parseAsArrayOf agree on every URL when nuqs uses its default , separator (a custom separator is not supported) and the element codec accepts the same text as nuqs's element parser. Element grammars can differ: parseAsInteger reads "1.5" as 1, while p.integer() rejects it, so nuqsArrayOf(p.integer()) drops that element.

Because a failing element is dropped rather than failing the key, a .catch() on nuqsArrayOf never fires for bad elements. Use .default() for the value of an absent key.

NoNuqsTwin

The compile-time rejection brand for codec shapes with no faithful nuqs translation.

interface NoNuqsTwin<Reason extends string> {
  readonly [noTwinReason]: Reason;
}

Some shapes cannot be translated without changing their meaning, and the adapter refuses at the call site rather than silently conflating semantics. The mechanism: incompatible arguments are intersected with NoNuqsTwin, whose single symbol-keyed property is unforgeable (the symbol is not exported), so no runtime value inhabits the type and the call fails to compile — with the Reason literal surfacing in the error text.

Rejected shapes:

  • Codec output includes null. nuqs reserves null to mean "absent or unparseable", so a codec whose legitimate output includes null would have its null indistinguishable from a parse failure on the nuqs side.
  • rawSearch routes — the whole search object is validated by one schema; there are no per-key codecs to derive from.
  • Search-less routes and empty search configs — no keys to derive parsers for.
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">'.

The null-output rejection is type-level only: a codec's output type is a phantom (~out), so there is no runtime probe, and a null that slips past the types degrades to nuqs's native null semantics. The structural rejections (non-codec values, rawSearch, empty configs) do have runtime backstops — plain-JavaScript callers get the same judgments as ParamourErrors.

  • NuqsParserMap<S extends SearchConfig> — the parser map nuqsParsers derives from a whole search config: { [K in keyof S]: NuqsParserOf<S[K]> }, with the readonly modifier a route's const-inferred search slot carries stripped, so route-derived and bare-config-derived maps have the identical shape.
  • NuqsParserOf<C extends AnyCodec> — the parser type nuqsParser derives from one codec: a multi (repeated-key) parser with defaultValue: [] for arity-many codecs, a withDefault (non-nullable) single parser for value-form defaults, and a nullable single parser otherwise. Codecs built with p.* always carry the precise state; a codec whose default form the types can't see falls to the nullable branch — the safe reading.

On this page