Geschrieben von Huifer, Solo-Entwickler und Maintainer von TanStack Ship. Zwischen Januar und Juni 2026 habe ich 9 TanStack Start Apps auf Cloudflare Workers ausgeliefert – jede einzelne stützte sich auf TanStack Routers typsicheres Routing als Rückgrat. Dieser Leitfaden ist das, was ich mir am ersten Tag von App #1 gewünscht hätte – die 12 Patterns, die den Kontakt mit echten Nutzern überlebt haben, die 4 Anti-Patterns, die ich kein zweites Mal ausliefere, und die exakte Migrationsrechnung für den Umstieg von React Router v6.
Verifizierte Quellen: TanStack Router Docs · TanStack Ship GitHub · TanStack Start Docs Zuletzt aktualisiert: 2026-07-02 · Changelog
TL;DR: Typsicheres Routing bedeutet, dass der TypeScript-Compiler jede URL kennt, die Ihre App ausliefert, jeden Param, den jede URL entgegennimmt, und jeden Search-Wert, den jede URL akzeptiert – und sich weigert zu kompilieren, wenn Sie einen davon falsch schreiben. TanStack Router ist der einzige React Router 2026, der das auf Route-Tree-Ebene statt auf Hand-typed-Wrapper-Ebene liefert. Dieser Leitfaden deckt die gesamte Oberfläche in einem Stück ab: Route Trees, Navigation, Search Params, Loaders, Context, File-Based vs. Code-Based, Migration von React Router und die Produktions-Anti-Patterns, die ich nicht mehr ausliefere.
Was Typsicheres Routing wirklich bedeutet
In einem nicht-typsicheren Router (React Router v6, Next.js Pages Router, Remix vor v2) sind Routen Strings. Sie schreiben <Link to="/users/$id"> und wenn $id falsch geschrieben ist oder das dynamische Segment in $userId umbenannt wurde, erfahren Sie es erst in der Produktion, wenn der Nutzer auf einer 404 landet. Search Params sind sogar noch schlimmer – sie kommen als URLSearchParams zurück und Sie parsen sie mit || und Ternaries, in der Hoffnung, dass der Nutzer keine malformed URL eingefügt hat.
Typsicheres Routing kehrt das um. Die Quelle der Wahrheit des Routers ist eine generierte TypeScript-Datei, die jede Route, deren Params und deren Search-Schema auflistet. Wenn Sie ein <Link> schreiben oder navigate() aufrufen, prüft der Compiler gegen diese Quelle der Wahrheit und lässt Sie keine defekte Referenz ausliefern. In 9 Produktions-Apps 2026 hat dies die gesamte Klasse von „Broken-Link"-Bugs eliminiert, die früher einmal pro Quartal in Sentry auftauchten.
Die drei Garantien, die TanStack Router bietet
- Kompilierzeit-Routenexistenz:
navigate({ to: '/blog/$slug' })kompiliert nur, wenn eine Route unter/blog/$slugregistriert ist. - Typisierte Path-Params:
params: { slug: post.slug }wird gegen die deklarierte Form der Route geprüft. Falscher Typ? Kompilierfehler. - Typisierte Search Params:
search: { ref: utmSource }wird gegen dasvalidateSearch-Schema der Route geprüft. Fehlender Key? Falscher Typ? Kompilierfehler.
Die dritte Garantie ist die, die in der Produktion am meisten bringt. Jeder andere Router, den ich vor 2026 ausgeliefert habe, hatte einen Bug im Bereich URLSearchParams-Parsing. TanStack Router schließt diese Angriffsfläche vollständig.
Warum das kein Wrapped Library ist
Sie können React Router v6 mit TypeScript-Helfern wrappen und partielle Sicherheit bekommen – die meisten Teams, die ich kenne, machen das mit einer routes.ts-Datei und einem handgeschriebenen Route-Typ. Das Problem ist, dass handgeschriebene Typen driften. Jemand fügt eine Route hinzu, vergisst routes.ts zu aktualisieren, und die Sicherheit ist stillschweigend weg. TanStack Router generiert seine Typinformationen aus dem tatsächlichen Route Tree zur Build-Zeit. Die Typen können nicht driften, weil sie aus derselben Quelle abgeleitet werden, die der Router zur Laufzeit verwendet.
Der Route Tree: Quelle der Wahrheit
Jede TanStack Router App hat eine generierte routeTree.gen.ts-Datei. Diese Datei wird generiert, nicht geschrieben. Sie ist die Quelle der Wahrheit sowohl für den Runtime-Router als auch für die TypeScript-Typen. Zwei Patterns sind hier relevant.
File-Based Route Generation
Ich verwende den @tanstack/router-vite-plugin (oder das Äquivalent für TanStack Start), um den Route Tree aus dem Dateisystem zu generieren. Die Konvention ist:
// src/routes/blog/$slug/index.tsx
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/blog/$slug/')({
loader: async ({ params }) => fetchPost(params.slug),
component: BlogPostPage,
});
function BlogPostPage() {
const { slug } = Route.useParams();
const post = Route.useLoaderData();
return <article>{post.title}</article>;
}
Der Dateipfad /blog/$slug/index.tsx wird zur Route /blog/$slug. Das $slug-Segment ist als string in Route.useParams() typisiert. Wenn Sie die Datei in $postId umbenennen, ändert sich der Typ automatisch – und jeder Consumer, der params.slug referenziert hat, bricht zur Kompilierzeit, nicht zur Laufzeit.
Code-Based Route Generation
Für kleinere Apps oder für Routen, die komplexe Konfiguration benötigen, können Sie den Tree von Hand aufbauen:
import { createRootRoute, createRoute } from '@tanstack/react-router';
const rootRoute = createRootRoute({ component: RootLayout });
const blogIndexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/blog',
component: BlogIndex,
});
const blogPostRoute = createRoute({
getParentRoute: () => blogIndexRoute,
path: '$slug',
loader: ({ params }) => fetchPost(params.slug),
component: BlogPostPage,
});
export const routeTree = rootRoute.addChildren([
blogIndexRoute.addChildren([blogPostRoute]),
]);
Ich bevorzuge File-Based für jede App mit mehr als ~10 Routen. Handgebaute Trees sind in Ordnung für Embed-Style-Apps, wo Sie einen kleinen, expliziten Router wollen. Die Typgarantien sind in beiden Fällen identisch.
Generierte Typen sind nicht verhandelbar
In allen 9 Apps wird die generierte routeTree.gen.ts-Datei ins Repo eingecheckt. In CI führe ich tsc --noEmit gegen den generierten Tree aus. Das erste Mal, als ich vergessen habe, eine Regen einzuchecken, hatte ein Teamkollege ein 30-minütiges Mystery, das sich als fehlende Route herausstellte. Wir haben einen CI-Check hinzugefügt; er ist seitdem nie mehr ausgelöst worden. Das ist die einzelne CI-Regel mit dem höchsten ROI in meinen Routing-Setups.
Typsichere Navigation: Die Core API
Drei Navigations-APIs decken jeden Use Case in meinen Apps ab. Alle drei sind vollständig gegen den Route Tree typisiert.
Link für deklarative Navigation
import { Link } from '@tanstack/react-router';
function PostCard({ post }) {
// Type-checked: '/blog/$slug' muss existieren, slug muss ein string sein
return <Link to="/blog/$slug" params={{ slug: post.slug }}>Mehr lesen</Link>;
}
Wenn /blog/$slug nicht im Route Tree existiert, verweigert TypeScript die Kompilierung. Wenn post.slug kein String ist (er ist undefined, sagen wir), verweigert TypeScript die Kompilierung. Das ist der ganze Punkt.
useNavigate für programmatische Navigation
import { useNavigate } from '@tanstack/react-router';
function CheckoutButton() {
const navigate = useNavigate();
return (
<button
onClick={() =>
navigate({
to: '/checkout/$cartId',
params: { cartId: cart.id },
search: { ref: utmSource }, // ebenfalls typisiert
})
}
>
Zur Kasse
</button>
);
}
Das search-Feld hier erfordert, dass die Zielroute validateSearch deklariert. Wenn sie es nicht tut, können Sie keine Search Params übergeben – das Typsystem erzwingt es. Das ist die Art von Guardrail, die „Wir haben stillschweigend die UTM Source gedroppt"-Bugs verhindert.
redirect in Loaders
import { redirect } from '@tanstack/react-router';
export const Route = createFileRoute('/_authenticated/dashboard')({
beforeLoad: async ({ context, location }) => {
if (!context.session) {
throw redirect({
to: '/login',
search: { redirect: location.href },
});
}
},
});
redirect() ist ebenfalls typisiert – to und search werden auf dieselbe Weise geprüft. Ich verwende dies für jede Auth-Prüfung, jede Rollenprüfung und jedes „Stale Resource"-Redirect. Der Compiler erzwingt, dass ich nicht auf eine 404 redirecte.
Typsichere Search Params mit Zod
Search Params sind der fehleranfälligste Teil jedes Routers. URLSearchParams ist von Specifikation her ungetypt. TanStack Router behebt dies, indem jede Route ein validateSearch-Schema deklarieren kann, das bei jeder URL-Parsung ausgeführt wird.
Das Basis-Pattern
import { createFileRoute } from '@tanstack/react-router';
import { z } from 'zod';
const productsSearchSchema = z.object({
query: z.string().optional().default(''),
category: z.enum(['all', 'tools', 'templates']).optional().default('all'),
page: z.coerce.number().int().positive().optional().default(1),
sort: z.enum(['newest', 'price-asc', 'price-desc']).optional().default('newest'),
});
export const Route = createFileRoute('/products/')({
validateSearch: productsSearchSchema,
component: ProductsPage,
});
function ProductsPage() {
const { query, category, page, sort } = Route.useSearch();
// Jeder Wert ist typisiert, validiert und hat einen Default
return <ProductList query={query} category={category} page={page} sort={sort} />;
}
Drei Dinge passieren hier, die mit useSearchParams nicht passieren:
?page=abc(ungültige Zahl) wird gecoerct und fällt auf1zurück, nicht aufNaN.?category=hacked(ungültiges Enum) wird zu'all'sanitisiert, nicht zu einem Runtime-Crash.- Die Komponente sieht immer vollständig typisierte Werte – keine
?? ''-Chains.
In einer B2B-SaaS-App habe ich 14 Routen von Hand-rolled URLSearchParams-Parsing auf Zod-validierte Search migriert. Die Bug-Anzahl auf diesen Seiten sank von 6 pro Quartal auf 0 in der ersten Jahreshälfte 2026. Die Migrationskosten: ein halber Tag pro Route. Die Einsparung: fortlaufend.
Discriminated Unions für komplexen Filterzustand
Für Data-Table-Style-Filterung mit gemischten Bedingungen handhabt Zods discriminatedUnion die Komplexität:
const filterSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('text'), value: z.string() }),
z.object({ type: z.literal('range'), min: z.number(), max: z.number() }),
z.object({ type: z.literal('select'), values: z.array(z.string()) }),
]);
const searchSchema = z.object({
filters: z.array(filterSchema).optional().default([]),
sortField: z.string().optional().default('createdAt'),
sortDir: z.enum(['asc', 'desc']).optional().default('desc'),
});
Das komponiert sauber. Wenn der Nutzer einen Range-Filter hinzufügt, hat die URL ?filters[0][type]=range&filters[0][min]=10&filters[0][max]=100. Wenn er einen Text-Filter hinzufügt, ist es eine andere Form. Der Komponenten-Code verzweigt nie auf „Was für ein Filter ist das?" ohne dass das Typsystem es ihm sagt.
Search Params als bookmarkbarer Zustand
Da Search Params validiert und typisiert sind, dienen sie doppelt als URL-persistente UI-Zustände:
const uiSearchSchema = z.object({
sidebarOpen: z.coerce.boolean().optional().default(true),
activeTab: z.string().optional().default('overview'),
selectedIds: z.array(z.string()).optional().default([]),
});
Nutzer können exakte Anwendungszustände bookmarken, teilen und dorthin zurückkehren. Für Admin-Tools ist das der Unterschied zwischen „Ich habe die Sidebar geschlossen und meine Filterauswahl beim Refresh verloren" und „Alles ist genau so, wie ich es verlassen habe". Auf der AuditShip-Admin-Tabelle eliminierte dieses einzelne Pattern Support-Tickets zu „verlorenen Filtern" von 14 pro Monat auf 0.
Typsichere Loaders und Route Context
Loaders werden ausgeführt, bevor eine Route rendert. TanStack Router typisiert jedes Argument, das ein Loader erhält: params, search, context, location und abortController.
Der Loader-Vertrag
export const Route = createFileRoute('/blog/$slug/')({
loader: async ({ params, context, abortController }) => {
const post = await fetch(`/api/posts/${params.slug}`, {
signal: abortController.signal, // bricht bei Route-Wechsel ab
});
if (!post.ok) throw notFound();
return { post };
},
});
params.slug ist als string typisiert, weil der Route-Pfad $slug deklariert. abortController ist automatisch als AbortController typisiert. Der Return-Typ des Loaders ({ post: Post }) wird abgeleitet und über Route.useLoaderData() mit vollständiger Typsicherheit exponiert.
Route Context für domänenübergreifenden Zustand
Layout-Routen können typisierten Context bereitstellen, den alle Child-Routen konsumieren:
// src/routes/_authenticated.tsx
export const Route = createFileRoute('/_authenticated')({
beforeLoad: async ({ location }) => {
const session = await getSession();
if (!session) {
throw redirect({ to: '/login', search: { redirect: location.href } });
}
return { session };
},
component: AuthenticatedLayout,
});
function AuthenticatedLayout() {
// session ist hier garantiert definiert – beforeLoad hat sonst geworfen
return <Outlet />;
}
Dann die Child-Routen:
// src/routes/_authenticated/dashboard.tsx
export const Route = createFileRoute('/_authenticated/dashboard')({
// context.session ist automatisch typisiert
loader: ({ context }) => fetchDashboardStats(context.session.orgId),
component: Dashboard,
});
In der größten meiner Apps eliminiert dieses Pattern etwa 30 useAuth()-Aufrufe und 60 if (!user) return null-Guards. Das Typsystem erzwingt die Auth-Prüfung auf Routing-Ebene, nicht auf Komponenten-Ebene.
Der Performance-Gewinn: Loader Caching
TanStack Router cached Loader-Ergebnisse. Wenn ein Nutzer von /blog/$slug zu /about navigiert und zurück, wird der Loader nicht erneut ausgeführt, wenn die Daten noch frisch sind. Ich konfiguriere das global:
export const router = createRouter({
routeTree,
defaultPreloadStaleTime: 30_000, // 30s Cache
defaultPreload: 'intent', // Hover/Focus-Preload
});
In einer App verwandelte dies ein „Zurück klicken → 400ms Spinner"-Pattern in „Zurück klicken → sofortiges Paint" ohne, dass ich irgendeinen Cache-Code schreiben musste. Die Typen sind dieselben; die Runtime wurde einfach schneller.
File-Based vs. Code-Based: Wann welches verwenden
Beide Ansätze bieten identische Typgarantien. Der Unterschied liegt in Ergonomie und Auffindbarkeit.
File-Based gewinnt für
- Apps mit 10+ Routen
- Teams, bei denen neue Beitragende Routen schnell finden müssen
- Apps, in denen Routen eine natürliche Ordnerstruktur haben (
/blog/$slug/comments/$commentIdist offensichtlich) - Solo-Founder, die wollen, dass das Dateisystem die Dokumentation ist
Code-Based gewinnt für
- Embedded Widgets mit kleiner Oberfläche
- Autoren von Bibliotheken, die einen Router als Teil eines UI-Kits ausliefern
- Apps, in denen Routen dynamisch sind (aus einem CMS oder einer Config-Datei zur Laufzeit geladen)
- Apps, in denen sich die Routing-Struktur pro Tenant unterscheidet
Ich default zu File-Based für jede TanStack Start App. Code-Based ist reserviert für einmalige Embed-Komponenten und White-Label-Konfigurationen.
Migration von React Router v6
Drei Teams haben mich nach dem Migrationsplan gefragt. Hier ist die Rechnung vom letzten (April 2026, React Router v6 App mit 47 Routen).
Der Schritt-für-Schritt-Plan
- TanStack Router parallel zu React Router installieren. Beide Router können koexistieren; das Dateisystem und der Bundler müssen es nicht wissen.
- Den Route Tree aus React Routers Config generieren. Das ist einmalige manuelle Arbeit – übersetzen Sie
<Route path="/users/$id">increateRoute({ path: '/users/$id' }). - Leaf-Routen zuerst migrieren. Beginnen Sie mit Routen, die kein geteiltes Layout und keine Auth-Anforderungen haben. Die Auth-Boundary-Routen migrieren zuletzt, weil sie
beforeLoad-Setup benötigen. useNavigateund<Link>pro Komponente ersetzen. Die TanStack Router-Versionen haben dieselben Namen, aber unterschiedliche Argument-Shapes. Der Compiler sagt Ihnen, wo Sie ändern müssen.- Search Params auf Zod-Schemata umstellen. Das ist, wo die Typsicherheits-Auszahlung beginnt – jeder
URLSearchParams-Parser wird zu einem typisierten Schema. - React Router entfernen. Sobald jede Route TanStack Router verwendet, löschen Sie die Abhängigkeit.
Ehrliche Zeitschätzung
Die April-2026-Migration dauerte 4 Arbeitstage für die 47-Route-App. 60% dieser Zeit war Search-Params-Migration, weil die React Router-Version 14 Routen mit Hand-rolled useSearchParams-Parsing hatte. Die Route-Tree-Generierung selbst dauerte 90 Minuten. Wenn Ihre App bereits gut typisierte Search-Zustände hat, rechnen Sie mit 2–3 Tagen für 30 Routen.
Der Trade-Off, den Sie kennen sollten
Typsicherheit hat echte Kosten: Build-Zeit. Bei der 184-Route B2B-SaaS-App dauert tsc --noEmit 14 Sekunden. Der TanStack Router Code-Generation-Step fügt weitere 3,4 Sekunden hinzu. Das sind 17,4 Sekunden, bevor ich einen Typfehler sehe. Auf einem 2021 MacBook Air ist das schmerzhaft. Auf einem M-Serie-Mac ist es erträglich.
Wenn Sie < 30 Routen haben, werden Sie es nicht bemerken. Wenn Sie 100+ haben, ist der Trade-Off real. Ich habe erwogen, Type-Checks beim Speichern statt beim Build auszuführen, aber ich habe das noch nicht ausgeliefert.
Vier Produktions-Anti-Patterns
Dies sind die Patterns, die ich mindestens einmal ausgeliefert habe und nicht noch einmal ausliefere.
Anti-Pattern 1: Route-Typen von Hand schreiben
Ich habe es in App #2 versucht, Route-Typen von Hand zu schreiben und Codegen zu vermeiden. Die Typen drifteten innerhalb von drei Wochen. Der Compiler begann, Tippfehler durchzulassen, die hätte abgefangen werden müssen. Ich habe die handgeschriebenen Typen gelöscht und Codegen wieder eingeschaltet. Kämpfen Sie nicht gegen den Generator.
Anti-Pattern 2: Loaders, die Strings werfen
// FALSCH – kein Typ, kein Sentry-Trace, kein Recovery-Path
loader: async () => {
if (!session) throw 'not authenticated';
return fetchData();
}
Das richtige Pattern ist throw redirect({ to: '/login' }) für Auth, oder eine typisierte Error-Klasse, die die Route's errorComponent behandelt. Strings-as-Errors erscheinen nicht in Sentry, triggern nicht die Router's Error Boundary und tragen keinen Kontext.
Anti-Pattern 3: useSearchParams für neue Routen verwenden
Wenn Sie TanStack Router verwenden, verwendet jede neue Route Route.useSearch(). Das Legacy-useSearchParams von React Router funktioniert auch in TanStack Router, aber es umgeht das Typsystem und das Validierungs-Schema. Ich habe es aus jeder App entfernt, die ich anfasse; wenn Sie sich dabei ertappen, danach zu greifen, ist das ein Signal, dass Sie stattdessen ein validateSearch-Schema hinzufügen sollten.
Anti-Pattern 4: beforeLoad für Auth überspringen
Ich habe App #4 mit Komponenten-Level Auth-Guards ausgeliefert (if (!user) return <Redirect />), weil ich dachte, es sei einfacher. War es nicht. Komponenten-Guards laufen nach dem Data Fetching, was bedeutet, dass jede geschützte Route eine Netzwerk-Roundtrip an Requests verschwendet, die verworfen werden würden. beforeLoad läuft vor dem Laden der Route, was sowohl schneller als auch schwerer zu umgehen ist. Verwenden Sie beforeLoad.
Was ich noch nicht getestet habe
Die oben genannten Patterns funktionieren in meinem Maßstab – Peak-Traffic um 180 Req/s bei der ausgelastetsten App, mit 9 Apps insgesamt unter 12.000 monatlich aktiven Nutzern. Ich habe TanStack Router nicht bei 10k Req/s getestet. Die Codegen-basierte Typ-Inferenz könnte sich bei dieser Größe anders verhalten, besonders rund um die Route-Tree-Größe. Die Auth-Patterns verlassen sich darauf, dass KV am Edge schnell genug ist, was es in meinem Maßstab ist, aber bei höherer QPS möglicherweise nicht. Wenn Sie etwas Größeres ausliefern, testen Sie diese Patterns unter Ihrem eigenen Load-Shape, bevor Sie sie vollständig übernehmen.
Das ist das vollständige typsichere Routing-System, das ich in 9 TanStack Start Apps 2026 verwende. Das Pattern ist dasselbe, ob die App 5 oder 184 Routen hat: File-Based Generation, typisierte Navigation, Zod-validierte Search Params, Loaders, die typisierte Daten zurückgeben, und beforeLoad für domänenübergreifende Concerns wie Auth. Nichts davon ist neu – es ist, was den Kontakt mit echten Nutzern und echter Produktion überlebt hat.
Für einen genaueren Blick darauf, wie diese Patterns in einer ausgelieferten App zusammenspielen, lesen Sie TanStack Ships Router-Implementierung und das TanStack Router Advanced Patterns Deep Dive. Wenn Sie zwischen TanStack Router und den Alternativen wählen, gehen das TanStack Router vs. React Router Comparison und der TanStack Ship vs. ShipFast Breakdown die Typsicherheits-Trade-offs Seite an Seite durch. Für den breiteren 2026 SaaS-Stack ist der Tech Stack Decision Guide eine gute Begleitlektüre, und TanStack Router Best Practices 2026 deckt die Preloading- und Auth-Patterns ab, die auf diesem typsicheren Fundament aufbauen.