@paramour-js/nuqs
The nuqs adapter API.
@paramour-js/nuqs is a thin adapter (design-10) 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 & CompatibleCodec<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", (.().()));
return < ={() => void (..)} ={ ?? ""} />;
}The derivation rules, per codec shape:
| Codec shape | Derived 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
withDefaultwould lie. Absent readsnull— 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'snull. Anything that isn't a paramourParseError— including a throwing catch factory — propagates loudly.- Equality is wire-form equality. The parser's
eqcompares serialized wire strings — the same judgment paramour's own default elision makes — so nuqs'sclearOnDefaultand paramour's URL elision agree by construction, for every codec kind includingp.custom. - Value-form defaults are snapshotted once at derivation time. Mutating a reference-typed default (an array, say) after deriving is unsupported.
- Plain-JavaScript callers passing something that is not a codec get a
loud
ParamourErrorat 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.
function nuqsParsers<R extends AnyRoute>(
route: CompatibleRoute<R> & R,
): NuqsParserMap<R["~search"]>;
function nuqsParsers<S extends SearchConfig>(
config: CompatibleConfig<S> & 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:
// @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;
.q;
.tags;
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
NoNuqsTwinbelow.rawSearchroutes 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 loudParamourErrors at runtime for plain-JS callers. - Route vs. config detection is by value shape: only a route carries a
router-kind string on its
~routerbrand, 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.
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 reservesnullto mean "absent or unparseable", so a codec whose legitimate output includesnullwould have itsnullindistinguishable from a parse failure on the nuqs side. rawSearchroutes — 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);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.
Related types
NuqsParserMap<S extends SearchConfig>— the parser mapnuqsParsersderives from a whole search config:{ [K in keyof S]: NuqsParserOf<S[K]> }, with thereadonlymodifier a route'sconst-inferred search slot carries stripped, so route-derived and bare-config-derived maps have the identical shape.NuqsParserOf<C extends AnyCodec>— the parser typenuqsParserderives from one codec: a multi (repeated-key) parser withdefaultValue: []for arity-many codecs, awithDefault(non-nullable) single parser for value-form defaults, and a nullable single parser otherwise. A hand-typedCodec<…, "defaulted">whose default-elides flag was left at itsbooleandefault falls to the nullable branch — the safe reading.