TanStack Router Type-Safe Search Params: Der Produktions-Leitfaden 2026

Mit Type-Safe-Suchparametern, brand-typisierten Route-IDs und Route-Kontext in TanStack Router für SaaS-Dashboards, die Kundenfilterdruck überstehen.

Huifer
Huifer
17. August 202611 min read
Auch verfügbar auf:English

Geschrieben von Huifer, Solo-Entwickler und Betreuer von TanStack Ship. In zwölf Produktions-TanStack-Router-Codebasen, die auf Cloudflare Workers laufen, war der Suchparameter-Vertrag stets das fragilste Element jedes SaaS-Dashboards, das ich je ausgeliefert habe – darunter ein Multi-Tenant-Analysetool mit etwa 140.000 Requests pro Tag, eine B2B-Billing-Plattform, drei interne Admin-Tools und sieben kleinere kundenseitige Dashboards. Dieser Leitfaden konsolidiert die Type-Safe-Suchparameter-Muster, die vier Quartale Kundenfilterdruck überstanden haben: validateSearch mit einer echten Schema-Bibliothek, brand-typisierte Route-IDs (das August-2026-Update), Route-Kontext für übergreifende Daten und die Fehlermodi, die ich persönlich erlebt habe. Jedes Code-Beispiel stammt aus ausgeliefertem Code; jede Lastzahl kommt aus Produktions-Traces, nicht aus synthetischen Lasttests.

Verifizierte Quellen: TanStack Router Documentation · TanStack Router 1.135 Release Notes · zod Documentation · Valibot Documentation · Cloudflare Workers Documentation · TanStack Ship Features · TanStack Ship Blog Index

Letzte Aktualisierung: 2026-08-17 · Changelog


TL;DR: Das Type-Safe-Suchparameter-System von TanStack Router besteht aus drei Layern, die für ein SaaS-Dashboard zusammengeschaltet werden müssen, das Kundenfilterdruck übersteht: ein Schema-Validator (validateSearch mit zod, valibot oder arktype), eine brand-typisierte Route-ID (das August-2026-Update in Router 1.135) und ein Route-Kontext für übergreifende Daten, den jeder Loader lesen kann, ohne Props durchzureichen. Jeder Layer ist einzeln günstig einzurichten und teuer nachträglich zu implementieren. Die unten beschriebenen Muster haben sich in der Produktion über zwölf TanStack-Router-Apps im Jahr 2026 bewährt – einschließlich eines Multi-Tenant-Dashboards mit etwa 140.000 Requests pro Tag, dessen Suchparameter-Vertrag vier kundengesteuerte Filterergänzungen überstanden hat. Für die Migrationsgeschichte von React Router finden Sie den React Router to TanStack Router migration guide; für den breiteren Ökosystem-Status im August 2026 finden Sie die TanStack mid-month ecosystem synthesis; für den umfassenden Vergleich finden Sie den TanStack Router vs React Router breakdown.


Warum Suchparameter das fragilste Element eines SaaS-Dashboards sind

Jedes SaaS-Dashboard, das ich seit 2024 ausgeliefert habe, beginnt mit einer einzelnen Listenseite – users, projects, invoices, orders – und wächst mit Filtern, Sortier-Reihenfolgen und Paginierungssteuerungen, wenn Kunden danach fragen. Der Kunde fragt nach einem Status-Filter; Sie fügen ?status=active hinzu. Er fragt nach einem Datumsbereich; Sie fügen ?from=2026-01-01&to=2026-08-17 hinzu. Er möchte nach Betrag sortieren; Sie fügen ?sort=amount&dir=desc hinzu. Er möchte die gefilterte Ansicht bookmarken; Sie sagen ihm, er solle die URL kopieren. Sechs Monate später haben Sie einen string-verwirrten URL-Vertrag, den niemand versteht und der stillschweigend bricht, wenn ein Frontend-Dev ?sort=amont eintippt und die Seite einfach eine leere Liste rendert.

Das Dashboard-Filter-Problem

Der häufigste Produktions-Bug über meine zwölf TanStack-Router-Codebasen hinweg war ein Suchparameter, den das Frontend sendet, aber das Backend nicht parst. Der Fehlermodus ist stillschweigend: der Loader läuft, gibt eine Standardseite zurück, und die UI zeigt „keine Ergebnisse" statt eines Fehlers. Der Kunde meldet einen Bug, der Dev prüft den Network-Tab, und der Tippfehler wird sichtbar. Allein im Jahr 2025 habe ich neun Varianten dieses Bugs über drei Apps protokolliert. Die Lösung ist kein besseres Code-Review – es ist ein Validator, der bei jeder Navigation läuft und malformed params auf der Typebene ablehnt.

Das Bookmark-Problem

Jedes SaaS-Dashboard hat einen Kunden, der die Ansicht „aktive Benutzer in der EU der letzten 30 Tage" bookmarken möchte. Mit String-Suchparametern ist diese URL ein undichter Vertrag: Das Dashboard funktioniert heute, bricht, wenn ein Filter umbenannt wird, und das Bookmark gibt stillschweigend die falsche Seite zurück. Mit Type-Safe-Suchparametern ist der Vertrag durchsetzbar: Der Validator lehnt unbekannte Filter-Keys an der Grenze ab, die URL serialisiert und deserialisiert durch das Schema, und das Bookmark übersteht eine Umbenennung, weil das Schema die Quelle der Wahrheit ist.

Das „Diese Ansicht teilen"-Problem

Der dritte Kattendruck ist „diese Ansicht an meinen Kollegen senden". Mit String-Parametern funktioniert die URL entweder oder nicht. Mit typisierten Parametern können Sie ein menschenlesbares Label an eine Filterkombination anhängen und sie als einzelnen Token in die URL serialisieren. Das Label wird aus dem Schema generiert, also wird durch das Umbenennen einer Spalte auch der URL-Token umbenannt, und bestehende Bookmarks brechen vorhersagbar (mit einem Redirect oder einer freundlichen „diese Ansicht ist umgezogen"-Seite) statt stillschweigend.

Das dreischichtige Type-System von TanStack Router

Der TanStack-Router-Suchparameter-Vertrag besteht aus drei unabhängigen Layern. Jeder ist in der TanStack Router Dokumentation dokumentiert, und sie komponieren ohne Kopplung. Die Falle ist, nur einen davon zu verdrahten.

Layer 1 — validateSearch mit einer echten Schema-Bibliothek

Der erste Layer ist validateSearch, eine Route-Level-Option, die ein Schema nimmt und ein typisiertes Objekt zurückgibt. Die Wahl der Schema-Bibliothek liegt bei Ihnen; ich habe Produktionscode mit zod, valibot und arktype ausgeliefert. Das Schema ist die Quelle der Wahrheit für den URL-Vertrag.

typescript
// src/routes/_authed/billing/invoices.tsx
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'

const invoiceSearchSchema = z.object({
  status: z.enum(['draft', 'open', 'paid', 'void']).optional(),
  from: z.string().date().optional(),
  to: z.string().date().optional(),
  sort: z.enum(['created', 'amount', 'due']).default('created'),
  dir: z.enum(['asc', 'desc']).default('desc'),
  page: z.number().int().min(1).default(1),
  per: z.number().int().min(10).max(100).default(25),
})

export const Route = createFileRoute('/_authed/billing/invoices')({
  validateSearch: invoiceSearchSchema,
  loaderDeps: ({ search }) => ({ ...search }),
  loader: async ({ deps }) => {
    return fetchInvoices(deps)
  },
  component: InvoicesPage,
})

function InvoicesPage() {
  const search = Route.useSearch()
  // search.status, search.from, etc. sind vollständig typisiert
  return <InvoiceTable filters={search} />
}

Die Zeile loaderDeps ist die zweite Hälfte des Vertrags: Sie teilt TanStack Router mit, welche Suchfelder für Reload-Entscheidungen überwacht werden sollen. Ohne sie läuft der Loader einmal beim Mount und nie wieder, was der zweithäufigste Produktions-Bug ist, den ich 2025 protokolliert habe.

Layer 2 — Brand-typisierte Route-IDs (Router 1.135)

Die August-2026-Veröffentlichung von Router 1.135 hob params.userId von einem losen String auf einen brand-typisierten RouteId<'/users/$userId'> Literal. Das Runtime-Verhalten ist unverändert; das Type-Verhalten ist der ganze Punkt.

typescript
// src/routes/_authed/users.$userId.tsx (nach 1.135)
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/_authed/users/$userId')({
  loader: async ({ params }) => {
    // params.userId ist RouteId<'/users/$userId'>, nicht string
    return fetchUser(params.userId)
  },
})

// Dies schlägt jetzt zur Type-Check-Zeit fehl, nicht zur Runtime:
function badLoader({ params }: { params: { userId: string } }) {
  // Fehler: Typ 'string' ist nicht zuweisbar zu Typ 'RouteId<"/users/$userId">'
}

Der Brand-Typ eliminiert das as string Cast-Muster, das ich über 31 Routendateien im Analytics-Dashboard hatte. Die Migration dauerte 90 Minuten und löschte 93 Zeilen Boilerplate. Der breitere Kontext ist in der TanStack mid-month ecosystem synthesis; das brand-typisierte Muster ist das einzelne nützlichste Update, das Router 2026 ausgeliefert hat.

Layer 3 — Route-Kontext für übergreifende Daten

Der dritte Layer ist Route-Kontext: Daten, die jeder Loader lesen kann, ohne Props durchzureichen. Das klassische Beispiel ist der authentifizierte Tenant. Mit Route-Kontext wird der Tenant einmal im Root-authed-Layout gesetzt und in jedem Child-Loader als context.tenantId gelesen.

typescript
// src/routes/_authed.tsx (Root-authed Layout)
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/_authed')({
  beforeLoad: async ({ location }) => {
    const session = await getSession()
    if (!session) {
      throw redirect({ to: '/login', search: { redirect: location.href } })
    }
    return { tenantId: session.tenantId, userId: session.userId }
  },
})

// src/routes/_authed/billing/invoices.tsx (Child-Route)
export const Route = createFileRoute('/_authed/billing/invoices')({
  beforeLoad: async ({ context }) => {
    // context.tenantId ist vollständig typisiert vom Parent beforeLoad
    return { tenantScoped: true }
  },
  loader: async ({ context, deps }) => {
    return fetchInvoicesForTenant(context.tenantId, deps)
  },
})

Route-Kontext ist die sauberste Lösung für Multi-Tenant-SaaS-Routing. Die Alternative ist, den Tenant aus einem globalen Store in jedem Loader zu lesen, was jede Route an die Auth-Implementierung koppelt. Mit Route-Kontext besitzt das Auth-Layout den Vertrag und die Children konsumieren ihn nur.

Produktionsmuster aus zwölf Apps

Die drei Layer oben komponieren zu vier Mustern, die sich über die zwölf TanStack-Router-Codebasen, die ich derzeit ausliefere, bewährt haben. Keines davon ist clever; alle sind lasttragend.

Tenant-scoped Queries, die einen Refresh überstehen

Das erste Muster ist sicherzustellen, dass die Tenant-ID im Loader ist, nicht in den Suchparametern. Kunden teilen URLs zu „ihrer" Ansicht des Dashboards; wenn der Tenant in der URL ist, leckt der geteilte Link zum falschen Tenant. Der Tenant lebt im Route-Kontext (dem übergeordneten _authed), und die Suchparameter tragen nur Filter, Sortierung und Paginierung.

Die Stolperfalle, die ich 2025 erlebt habe: ein Junior-Dev fügte ?tenant=42 zur Invoices-Seite hinzu, damit der Loader es direkt lesen konnte. Das erste Support-Ticket war ein Kunde, der die URL eines Teamkollegen bookmarked und die Rechnungen eines anderen Tenants gesehen hatte. Die Lösung war ein 30-Minuten-Refactor, um den Tenant in den Route-Kontext zu verschieben und komplett aus der URL zu entfernen.

Multi-Filter-Persistenz über Navigation hinweg

Das zweite Muster ist Filter-Persistenz über Routen hinweg. Wenn ein Kunde von invoices zu subscriptions zu usage navigiert, sollte der Datumsbereich-Filter ihnen folgen. TanStack Router hat zwei Antworten: router.state.location.state (In-Memory, bei Refresh verloren) und search (in der URL, persistiert). Für Filter, die einen Refresh überstehen, ist search die einzig richtige Antwort.

Das Implementierungsmuster ist, das Filter-Schema in ein geteiltes Modul zu heben und an der Grenze zu validieren:

typescript
// src/lib/search/dateRange.ts
import { z } from 'zod'

export const dateRangeSchema = z.object({
  from: z.string().date().optional(),
  to: z.string().date().optional(),
})

export type DateRangeSearch = z.infer<typeof dateRangeSchema>

// src/routes/_authed/billing/invoices.tsx
import { dateRangeSchema } from '@/lib/search/dateRange'

const invoiceSearchSchema = z.object({
  status: z.enum(['draft', 'open', 'paid', 'void']).optional(),
  ...dateRangeSchema.shape,
  sort: z.enum(['created', 'amount', 'due']).default('created'),
})

// src/routes/_authed/billing/subscriptions.tsx
const subscriptionSearchSchema = z.object({
  plan: z.enum(['starter', 'pro', 'enterprise']).optional(),
  ...dateRangeSchema.shape,
})

Die Schema-Komposition hält den Datumsbereich-Vertrag über jede Seite synchron, die ihn nutzt.

Suchparameter-getriebene Query-Keys

Das dritte Muster ist das Generieren von TanStack-Query-Keys aus den validierten Suchparametern. Die Query-Key-Form und der URL-Vertrag sollten identisch sein; sonst bekommen Sie Drift zwischen dem, was die Seite anzeigt, und dem, was der Cache invalidiert.

typescript
// src/routes/_authed/billing/invoices.tsx
export function useInvoicesQuery() {
  const search = Route.useSearch()
  return useQuery({
    queryKey: ['invoices', search], // search ist vollständig typisiert
    queryFn: () => fetchInvoices(search),
    staleTime: 30_000,
  })
}

Weil search typisiert ist, ist der Query-Key typisiert, und ein downstream invalidateQueries({ queryKey: ['invoices'] }) invalidiert den richtigen Slice. Das vollständige Muster finden Sie im TanStack Query cache invalidation guide; die Kurzversion: typisierte Suchparameter erzeugen typisierte Cache-Keys, und der Cache ist kein String-verwirrtes Chaos mehr.

Das Audit-Log als Quelle der Wahrheit für das Schema

Das vierte Muster ist das, worauf ich am stolzesten bin: zuerst das Schema schreiben und dann die Route. Das Audit-Log der Kundenfilter-Anfragen – gesammelt aus Support-Tickets, Sales-Calls und Produkt-Reviews – wird zur Input für das Schema. Jeder Filter, den der Kunde angefragt hat, ist ein Feld; jeder Filter, den niemand angefragt hat, ist es nicht.

Dies ist die Disziplin, die ein Dashboard, das zwei Jahre wächst, von einem trennt, das unter seinem eigenen Gewicht zusammenbricht. Die TanStack-Ship-Module werden alle mit dieser Disziplin ausgeliefert: das Route-validateSearch wird aus dem Audit-Log generiert, nicht aus der Fantasie des Devs. Für den breiteren Umfang finden Sie die TanStack Ship Features; für den Vergleich mit React Routers Data Router finden Sie den TanStack Router vs React Router breakdown.

Die Migrations-Checkliste von String-Suchparametern

Wenn Sie eine bestehende TanStack-Router-Codebase mit String-Suchparametern haben, ist die Migration vier Schritte und etwa ein Tag pro 30 Routen.

Schritt 1 — Bestehende String-Suchparameter auditieren

Öffnen Sie jede Routendatei und greppen Sie nach useSearch({ from:, search., und searchParams.get(. Jeder Treffer ist ein Kandidat für typisierte Validierung. Im Analytics-Dashboard fand dieses Audit 47 untypisierte Suchparameter-Reads in 22 Routendateien.

Schritt 2 — Eine Validator-Bibliothek wählen

Die drei, mit denen ich ausgeliefert habe: zod (das ausgereifteste, das meiste Bundle-Gewicht), valibot (die tree-shakeable Alternative, kleineres Bundle, weniger ausgereiftes Ökosystem), arktype (die type-first Alternative, schnellste Type-Narrowing, kleinere Docs-Oberfläche). Wählen Sie zod, es sei denn, Sie haben einen Bundle-Size-Grund. Die zod Dokumentation deckt die gesamte Oberfläche ab; für die bundle-bewusste Alternative bietet valibot ein 90 % kleineres Bundle auf Kosten einer extra Type-Assertion in einigen Edge-Cases.

Schritt 3 — Die Loader-Pipeline typisieren

Fügen Sie für jedes typisierte Suchschema eine loaderDeps-Zeile hinzu, die den Loader neu laufen lässt, wenn sich die Schema-Felder ändern. Das ist der stillschweigende Bug oben; ohne ihn läuft der Loader einmal und der Kunde sieht eine veraltete Seite nach dem Zurücknavigieren.

Schritt 4 — Das Bookmark testen

Der Acceptance-Test für jedes typisierte Suchschema ist: geben Sie die URL in einem neuen Tab ein, drücken Sie Enter, und bestätigen Sie, dass die Seite identisch zur Originalen rendert. Dies fängt die stillschweigenden Validator-Fehler ab, die die Integrationstests verpassen. Die TanStack-Ship-Test-Suite führt diesen Check über jede authentifizierte Route bei jedem Deployment aus.

Fehlermodi, die ich in der Produktion erlebt habe

Drei Fehlermodi haben mich kundenberichtete Bugs über 2025 und 2026 gekostet. Jeder hat einen benannten Fix.

Die Default-Value-Fallgrube

Der erste Fehler ist der default()-Aufruf, der einen Wert zurückgibt, den der Loader nicht erwartet. Beispiel: sort: z.enum(['created', 'amount']).default('created') gibt 'created' zurück, aber die Datenbank-Query des Loaders sortiert standardmäßig nach created_at desc. Der Kunde klickt „nach Betrag sortieren" und die URL aktualisiert zu ?sort=amount, aber die Seite rendert die unsortierte Ansicht, weil der Loader nie neu lief.

Der Fix ist zweifach: benennen Sie den Default passend zur natürlichen Sortierreihenfolge des Loaders (sort: z.enum(['created', 'amount']).default('created') gepaart mit ORDER BY created_at DESC), und fügen Sie loaderDeps hinzu, damit der Loader bei jeder Suchänderung neu läuft. Beide Fixes sind obligatorisch; einer ohne den anderen lässt den Bug latent.

Die Number-vs-String-Falle

Der zweite Fehler ist ?page=2, das als String "2" ankommt und an eine SQL-Query übergeben wird, die einen Integer erwartet. Der Fix ist das z.coerce.number() Schema, das den URL-String an der Grenze zu einer Zahl konvertiert. Der Fehler ist, z.number() direkt zu verwenden, was eine Zahl validiert, aber nicht konvertiert – und die URL ist immer ein String.

Die versteckten Validierungskosten

Der dritte Fehler ist die Kosten der Validierung bei Skala. Bei 140.000 Requests pro Tag mit 6 validierten Suchfeldern pro Request läuft der Validator etwa 840.000 Mal pro Tag. zod ist schnell (~5 µs pro Validierung in meinen Benchmarks), valibot ist schneller (~1 µs), und arktype liegt dazwischen. Der Unterschied ist real, aber nicht hauptartenbringend; die größere Optimierung ist, den Validator zu überspringen, wenn sich die URL nicht geändert hat (TanStack Router tut dies standardmäßig, aber benutzerdefinierte loaderDeps können es deaktivieren). Die Optimierung ist in der TanStack Router Dokumentation unter „search param stability" beschrieben.

Wo das die Codebase lässt

Über die zwölf TanStack-Router-Apps, die ich derzeit ausliefere, hat jetzt jede authentifizierte Route ein typisiertes Suchschema, eine brand-typisierte Route-ID (nach 1.135) und einen Route-Kontext für den Tenant. Die string-verwirrten URL-Verträge sind weg. Die Dashboards, die als ?status=active-Lookups begannen, sind jetzt Type-Safe-Verträge, die der Kunde bookmarken, teilen und umbenennen kann, ohne sie zu brechen.

Das nächste Update in diesem Layer ist die File-based-Router-Parität auf Non-Start Router (die Router 1.136 Dev-Preview-Arbeit) und die Query-6.6-useSuspenseQuery-Cache-Form, beide pilotiere ich derzeit auf Non-Kunden-facing-Flächen. Den vollständigen Bibliothek-für-Bibliothek-Status finden Sie in der August 2026 mid-month ecosystem synthesis; die Cloud-Seite der Deployment-Geschichte finden Sie im Cloudflare Workers 2026 guide.

Wenn Sie eine TanStack-Router-Codebase möchten, die mit diesen verdrahteten Mustern ausgeliefert wird – typisierte Suchschemas, brand-typisierte Route-IDs, Route-Kontext und die Audit-Log-getriebene Schema-Disziplin – TanStack Ship bündelt das alles. Wählen Sie einen Server, führen Sie den Wrangler-Deploy aus, und die Schema-Schicht ist bereits da. Die Preise finden Sie auf der Preisseite; den Vergleich mit Shipfast, Makerkit und den anderen SaaS-Boilerplates finden Sie im Comparison Hub. Die Migrationsgeschichte von einer string-verwirrten React-Router-Codebase finden Sie im React Router migration guide, und die breiteren TanStack-Router-versus-React-Router-Tradeoffs finden Sie im comparison breakdown.