The one idea — path params identify WHICH, search params are its typed state
Path params ($postId) live inside the pathname and identify which thing
(/posts/42). Search params (?page=2) live in the query string and carry
typed, validated state. TanStack Router's headline differentiator: attach a Zod (or valibot / standard-schema) validateSearch
to a route and the URL ?page=2 is parsed, validated, defaulted AND fully typed at
useSearch() — it arrives as {page: 2} (number), not '2'
(string), because the default parser is JSON-first.
Two param types — two roles, two readers
| param type | where in the URL | declared via | read with | typed? |
|---|---|---|---|---|
| path param | /posts/42 — a segment of the pathname |
$param in file path (e.g. posts.$postId.tsx) |
Route.useParams() / useParams() |
yes — by the generated route tree |
| search param | ?page=2&sort=desc — the query string |
a validateSearch schema (Zod / standard-schema) |
Route.useSearch() / useSearch() |
yes — validated and typed |
In react-router, search params are raw URLSearchParams strings you hand-parse and hand-coerce.
TanStack turns them into a typed, validated store — the URL becomes a serializable slice of app state.
The route's schema — src/routes/products.tsx (the source of truth)
validateSearch accepts a Zod object directly (Zod v4) or via the zodValidator adapter (Zod v3), or any standard-schema
(valibot / ArkType). The live validator below is a faithful, zero-dep simulation of this exact schema.
Live search-param validator — edit the query string
Type a query string. The parser is JSON-first (TanStack's default): each value is run through
JSON.parse, so page=2 becomes the number 2,
tags=["a","b"] becomes an array, while sort=desc stays a string
(it's not valid JSON). Then the schema validates each field and fills defaults. Try the gold cases:
?page=3 → {page: 3} number · ?page=abc → FAIL.
/products1 · useSearch() — parsed + typed
2 · validation, per field
3 · defaults applied
—
—
Path params — useParams(), typed by the route tree
The file name posts.$postId.tsx declares a typed postId. Matching
/posts/42 yields { postId: '42' } at Route.useParams() — or a number if you add params.parse.
Edit the segment and watch the typed params object update:
/posts/
/edit
params.parse lets a route parse a numeric segment (returning false falls through to the next candidate, e.g. a
$slug route). Navigating is type-checked: <Link to="/posts/$postId" params={{'{'}} postId: '123' {'}'}} />.
.default() the
<Link> search prop is optional; without it, TypeScript requires it. An invalid ?page=abc
throws (renders the route's errorComponent, error.routerCode === 'VALIDATE_SEARCH') — by design.
Cross-refs (🔗): tanstack_start_overview (the stack this routing layer powers) · type-safety · file-based routing · navigation/links (sibling bundles).