Search Param Validation — TanStack Router’s killer feature

[check: …]
📖 guide (.md) ← react deep dive
📖 Pair this live validator with the companion guide (.md) — this page is the rendered ground truth. ↗ Builds on frontend/tanstack-start: path & search params (the basics of path/search params — this bundle goes to the validation schema engine).

raw URLSearchParams → typed, validated, defaulted

A URL search string like ?page=2&sort=desc is raw text — URLSearchParams hands you {{page:"2", sort:"desc"}}: everything is a string, nothing is checked, nothing has a default. TanStack Router’s validateSearch replaces that with a schema (Zod / Valibot / ArkType). The router runs a pipeline — parse → coerce → validate → default — and the component reads route.useSearch() to get a real {'{ page: number, sort: "asc"|"desc", filter?: string }'}. Invalid URLs are rejected (or recovered with fallbacks), never silently passed through.

stagewhat happensexample
parse URL search string → object of raw strings ?page=2{'{ page:"2" }'}
coerce declared type converts the string z.number(): "2"2
validate constraints checked; reject on failure .min(1), z.enum([...])
default missing key → schema default no page1
type validator return type flows to consumers route.useSearch() is fully typed

1 · the schema validator you write (edit me)

searchSchema is the single source of truth (this stands in for z.object({'{...}'})). validateSearch() is the engine: it walks each field, coerces numbers, checks enum/min constraints, and fills defaults for missing keys — returning {'{ data, errors }'}, exactly like a Zod parse result.

2 · Babel compiles JSX → element tree

<SearchValidationDemo/> becomes React.createElement(SearchValidationDemo). The schema + validator run as plain JS on every keystroke; React only re-renders the verdict when the parsed input changes.

// (hit "compile & render" to see Babel's output)

3 · live search-param validator (proven)

Edit page / sort / filter above and watch the verdict flip. Try page=invalid (number error), sort=unknown (enum error), or clear a field (default applied). The gold-check drives all four cases automatically.

schema params: · gold: valid(2) → number error → enum error → defaults(1)

intent → schema pattern

intentpatternwhy
required number param z.number().int().min(1) rejects non-numbers and out-of-range; no silent "2"
default when missing .default(1) applied only when the key is absent (parse-level)
recover when invalid fallback(z.number(), 1) / .catch(1) keeps the route alive instead of throwing to errorComponent
constrain to a set z.enum(['asc','desc']) type narrows to the union; unknown value → error
optional param .optional() omitted key → undefined (not an error)
read in a component route.useSearch() typed by the schema’s output; never raw strings
handle invalid URL errorComponent: ({{ error }}) => ... shown when validation throws; reset via router.navigate