TanStack Router conventions
Stack conventions for TanStack Router apps (Vite SPA or the router layer of TanStack Start). Pair with tanstack-start-conventions only when the app uses Start (SSR, server functions, API routes).
When to apply
- Scaffolding a TanStack Router SPA (
@tanstack/react-router + Vite)
- Adding or changing UI routes, loaders, or
validateSearch
- Configuring
router.tsx (parseSearch, stringifySearch, trailing slash)
- Viewport URL param
?map=zoom/lat/lng (parse/serialize, validateSearch)
- Loader + React Query integration (client-side)
- Typed
Link, useParams / useSearch, getRouteApi
- Installing the TanStack devtools debug panel
Not in this skill: createServerFn, .server.ts / .functions.ts, selective SSR, RSC, API server.handlers — those are tanstack-start-conventions.
Reference reading order
- router-search-serialization.md — required
parseSearch / stringifySearch, pretty JSON URLs, per-param encodings, optional jsurl
- map-search-param.md —
?map=zoom/lat/lng (tilda-geo format, rounding, validateSearch)
- params-search-ui-routes.md — Zod
validateSearch on UI routes
- router-and-query.md — Query options, loaders, router defaults
- router-typescript.md — typed hooks,
from, Link params, Register
- devtools.md — unified TanStack debug panel
Non-negotiable rules
| Topic |
Rule |
| Validation |
Zod 4 for path params and UI search; export search schema when reused outside the route; Route.useSearch() for types — no manual casts |
| Router search URLs |
parseSearch + stringifySearch in router.tsx (pretty JSON baseline); trailingSlash: 'never' + root trailing-slash redirect; per-param compact/JSON encodings; jsurl only for large objects — router-search-serialization.md |
| Map viewport URL |
?map=zoom/lat/lng via shared parseMapParam / serializeMapParam; round on serialize; never encodeURIComponent the value; keep map out of loaderDeps — map-search-param.md |
| jsurl (optional) |
Large nested route params only → key in jsurlSearchKeys + search registry; reuse jsurlParse / jurlStringify |
| Query-backed UI |
Loader primes cache; component uses useQuery / useSuspenseQuery — not useLoaderData alone |
| Devtools panel |
TanStackAppDevtools in components/shared/devtools/ — inline panels; prod strip via @tanstack/devtools-vite (see devtools.md) |
Quick decisions
Loader vs Query: Shared, invalidatable, multi-route data → *QueryOptions + ensureQueryData in loader + useSuspenseQuery in UI. One-off page data → loader return value + useLoaderData.
Multi-query joins: Prefer a server/API join. On the client, default to composing in a wrapper hook; only put derivation in queryFn / fetchQuery when many consumers share an expensive join — router-and-query.md.
Search writes: Prefer replace: true for filters/toggles; set keys to undefined to clear; use a route-local updateSearch wrapper — router-search-serialization.md.
Related skills
| Topic |
Skill |
| TanStack Start (SSR, server) |
tanstack-start-conventions |
| Auth / session (Start) |
tanstack-start-auth |
| Zustand |
zustand-state-management |
| nuqs (Next.js / legacy only) |
nuqs |
| React TS / Compiler |
react-dev |
| Map component + URL sync |
react-map-gl |
1---2name: tanstack-router-conventions3description: TanStack Router conventions for FixMyBerlin/FMC (SPA or Start): validateSearch with Zod, parseSearch/stringifySearch for clean share URLs, map=zoom/lat/lng search param, loader + React Query patterns, typed Link/params, and TanStack devtools. Use when scaffolding a Vite + TanStack Router SPA, wiring router.tsx search serialization, route search params (including map viewport URLs), loaders, or Query integration — without TanStack Start server/SSR topics.4---56# TanStack Router conventions78Stack conventions for **TanStack Router** apps (Vite SPA or the router layer of TanStack Start). Pair with `tanstack-start-conventions` only when the app uses Start (SSR, server functions, API routes).910## When to apply1112- Scaffolding a TanStack Router SPA (`@tanstack/react-router` + Vite)13- Adding or changing UI routes, loaders, or `validateSearch`14- Configuring `router.tsx` (`parseSearch`, `stringifySearch`, trailing slash)15- Viewport URL param `?map=zoom/lat/lng` (parse/serialize, validateSearch)16- Loader + React Query integration (client-side)17- Typed `Link`, `useParams` / `useSearch`, `getRouteApi`18- Installing the TanStack devtools debug panel1920**Not in this skill:** `createServerFn`, `.server.ts` / `.functions.ts`, selective SSR, RSC, API `server.handlers` — those are `tanstack-start-conventions`.2122## Reference reading order23241. [router-search-serialization.md](references/router-search-serialization.md) — **required** `parseSearch` / `stringifySearch`, pretty JSON URLs, per-param encodings, optional jsurl252. [map-search-param.md](references/map-search-param.md) — `?map=zoom/lat/lng` (tilda-geo format, rounding, validateSearch)263. [params-search-ui-routes.md](references/params-search-ui-routes.md) — Zod `validateSearch` on UI routes274. [router-and-query.md](references/router-and-query.md) — Query options, loaders, router defaults285. [router-typescript.md](references/router-typescript.md) — typed hooks, `from`, `Link` params, `Register`296. [devtools.md](references/devtools.md) — unified TanStack debug panel3031## Non-negotiable rules3233| Topic | Rule |34| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |35| Validation | Zod 4 for path params and UI search; export search schema when reused outside the route; `Route.useSearch()` for types — no manual casts |36| Router search URLs | **`parseSearch` + `stringifySearch`** in `router.tsx` (pretty JSON baseline); `trailingSlash: 'never'` + root trailing-slash redirect; per-param compact/JSON encodings; jsurl **only** for large objects — [router-search-serialization.md](references/router-search-serialization.md) |37| Map viewport URL | `?map=zoom/lat/lng` via shared `parseMapParam` / `serializeMapParam`; round on serialize; never `encodeURIComponent` the value; keep `map` out of `loaderDeps` — [map-search-param.md](references/map-search-param.md) |38| jsurl (optional) | Large nested route params only → key in `jsurlSearchKeys` + search registry; reuse `jsurlParse` / `jurlStringify` |39| Query-backed UI | Loader primes cache; component uses `useQuery` / `useSuspenseQuery` — not `useLoaderData` alone |40| Devtools panel | `TanStackAppDevtools` in `components/shared/devtools/` — inline panels; prod strip via `@tanstack/devtools-vite` (see [devtools.md](references/devtools.md)) |4142## Quick decisions4344**Loader vs Query:** Shared, invalidatable, multi-route data → `*QueryOptions` + `ensureQueryData` in loader + `useSuspenseQuery` in UI. One-off page data → loader return value + `useLoaderData`.4546**Multi-query joins:** Prefer a server/API join. On the client, default to composing in a wrapper hook; only put derivation in `queryFn` / `fetchQuery` when many consumers share an expensive join — [router-and-query.md](references/router-and-query.md#derived--combined-query-data).4748**Search writes:** Prefer `replace: true` for filters/toggles; set keys to `undefined` to clear; use a route-local `updateSearch` wrapper — [router-search-serialization.md](references/router-search-serialization.md).4950## Related skills5152| Topic | Skill |53| ---------------------------- | ---------------------------- |54| TanStack Start (SSR, server) | `tanstack-start-conventions` |55| Auth / session (Start) | `tanstack-start-auth` |56| Zustand | `zustand-state-management` |57| nuqs (Next.js / legacy only) | `nuqs` |58| React TS / Compiler | `react-dev` |59| Map component + URL sync | `react-map-gl` |