@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", (.().()));
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.
- 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 ownwithDefaulton the derived parser inside the component. The derived wire-formeqstill applies, soclearOnDefaultdrops the value from the URL whenever it equals the runtime default — no hand-writtennullelision in your setters. See the guide. - 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.
// 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:
// @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.
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.csv | nuqsArrayOf (and nuqs) | |
|---|---|---|
| comma inside an element | SerializeError at link-build time | escaped as the literal text %2C |
| element that fails to parse | the whole key fails (.catch() recovers it) | that element is dropped, the rest kept |
empty segment (a,,b) | parse failure | handed 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 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. Codecs built withp.*always carry the precise state; a codec whose default form the types can't see falls to the nullable branch — the safe reading.