path & search params

[check: …]
📖 Read the full guide — typed path params, validated search params with JSON-first parsing, and the full Zod schema. This page is the interactive companion.

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 typewhere in the URLdeclared viaread withtyped?
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.

import { createFileRoute } from '@tanstack/react-router'; import { z } from 'zod'; const productSearchSchema = z.object({ q: z.string().default(''), // missing -> '' page: z.number().int().positive().default(1), // missing -> 1 ; 'abc' -> error sort: z.enum(['asc', 'desc']).default('asc'), // missing -> 'asc' tags: z.array(z.string()).optional(), // optional -> undefined }); export const Route = createFileRoute('/products')({ validateSearch: productSearchSchema, // parse + validate + default + type, in one line component: ProductsPage, }); function ProductsPage() { const { q, page, sort, tags } = Route.useSearch(); // page: number, sort: 'asc'|'desc', … return <ProductList page={page} sort={sort} />; // compiler knows the types }

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.

query string on /products
?page=3  (gold: number) ?page=abc  (gold: fail) (empty)  (gold: defaults) ?q=hello&page=5&sort=asc ?sort=banana  (enum fail) ?page=2.5  (int fail) ?tags=["x",5]  (array elem fail)

1 · useSearch() — parsed + typed

2 · validation, per field

3 · defaults applied

TanStack — useSearch() (typed + validated)
raw URLSearchParams (everything is a string)

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:

pathname
/posts/ /edit
matched route
/posts/$postId/edit
file: posts.$postId.edit.tsx
useParams() — string (default)
{ postId: '42' }
postId: string
with params.parse — typed number
{ postId: 42 }
postId: number

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' {'}'}} />.

The reframe: path params identify the resource; search params are its typed, validated state. TanStack makes the URL a fully-typed store — and search params are part of route identity: with .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).