Claude Code for TanStack Router: Type-Safe Routing with File-Based Routes — Claude Skills 360 Blog
Blog / Frontend / Claude Code for TanStack Router: Type-Safe Routing with File-Based Routes
Frontend

Claude Code for TanStack Router: Type-Safe Routing with File-Based Routes

Published: February 15, 2027
Read time: 8 min read
By: Claude Skills 360

TanStack Router provides fully type-safe routing — route params, search params, and loader data all carry TypeScript types inferred from the route definition. File-based routing generates the route tree automatically from a routes/ directory. createFileRoute("/orders/$orderId") defines a route with a typed $orderId param. loader fetches data server-side (or at navigation time) — useLoaderData() returns the fully-typed result. Search params are validated with Zod and typed — useSearch() returns the validated type. Nested layouts compose route segments. Route guards redirect unauthenticated users. RouterDevtools visualizes the route tree and pending states. Claude Code generates TanStack Router route files, loader functions, search param schemas, layout components, and the Vite configuration for file-based route generation.

CLAUDE.md for TanStack Router

## TanStack Router Stack
- Version: @tanstack/react-router >= 1.65, @tanstack/router-devtools >= 1.65
- File-based: use @tanstack/router-vite-plugin — generates routeTree.gen.ts automatically
- Routes: src/routes/ directory — _layout.tsx for layouts, index.tsx for root
- Params: createFileRoute("/orders/$orderId") — $orderId inferred in useParams
- Loaders: route.loader = async ({ params, context }) => {...} — prefetched at navigation
- Search: searchParams with validateSearch: zodValidator — typed search
- Guards: beforeLoad throws redirect(302, { to: "/login" }) for auth
- Data: useLoaderData() — fully typed return from loader function

Vite Configuration

// vite.config.ts — TanStack Router file-based routing
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
import { TanStackRouterVite } from "@tanstack/router-vite-plugin"

export default defineConfig({
  plugins: [
    TanStackRouterVite(),  // Auto-generates routeTree.gen.ts
    react(),
  ],
})

Route Files

// src/routes/__root.tsx — root layout route
import { createRootRouteWithContext, Outlet } from "@tanstack/react-router"
import { TanStackRouterDevtools } from "@tanstack/router-devtools"
import type { QueryClient } from "@tanstack/react-query"

interface RouterContext {
  queryClient: QueryClient
  auth: {
    isAuthenticated: boolean
    userId: string | null
  }
}

export const Route = createRootRouteWithContext<RouterContext>()({
  component: RootLayout,
})

function RootLayout() {
  return (
    <>
      <nav>
        <a href="/">Home</a>
        <a href="/orders">Orders</a>
        <a href="/account">Account</a>
      </nav>
      <main>
        <Outlet />
      </main>
      {import.meta.env.DEV && <TanStackRouterDevtools />}
    </>
  )
}
// src/routes/_auth.tsx — authenticated layout wrapper
import { createFileRoute, redirect, Outlet } from "@tanstack/react-router"

export const Route = createFileRoute("/_auth")({
  beforeLoad: ({ context }) => {
    if (!context.auth.isAuthenticated) {
      throw redirect({
        to: "/login",
        search: { redirect: location.href },
      })
    }
  },
  component: AuthLayout,
})

function AuthLayout() {
  return (
    <div className="authenticated-layout">
      <Outlet />
    </div>
  )
}
// src/routes/_auth/orders/index.tsx — orders list route
import {
  createFileRoute,
  useLoaderData,
  Link,
  useSearch,
} from "@tanstack/react-router"
import { z } from "zod"
import { zodValidator } from "@tanstack/zod-adapter"
import { fetchOrders } from "@/api/orders"

// Typed search params with Zod validation
const ordersSearchSchema = z.object({
  status: z.enum(["all", "pending", "shipped", "delivered"]).default("all"),
  page: z.number().int().positive().default(1),
  search: z.string().optional(),
})

export const Route = createFileRoute("/_auth/orders/")({
  // Validates and types search params
  validateSearch: zodValidator(ordersSearchSchema),

  // Loader: fetches data before rendering, React Query-aware
  loader: async ({ context, deps: { status, page, search } }) => {
    const { userId } = context.auth
    return context.queryClient.ensureQueryData({
      queryKey: ["orders", userId, { status, page, search }],
      queryFn: () => fetchOrders({ customerId: userId!, status, page, search }),
    })
  },

  // Memoize loader based on search params
  loaderDeps: ({ search: { status, page, search } }) => ({ status, page, search }),

  component: OrdersPage,
})

function OrdersPage() {
  // Fully typed — inferred from loader return type
  const { orders, totalCount, pageCount } = useLoaderData({ strict: false })

  // Typed search — validated by Zod schema
  const { status, page, search } = useSearch({ strict: false })
  const navigate = useNavigate()

  return (
    <div>
      <div className="filters">
        <select
          value={status}
          onChange={e => navigate({ search: s => ({ ...s, status: e.target.value as any }) })}
        >
          <option value="all">All</option>
          <option value="pending">Pending</option>
          <option value="shipped">Shipped</option>
        </select>

        <input
          value={search ?? ""}
          onChange={e => navigate({ search: s => ({ ...s, search: e.target.value || undefined }) })}
          placeholder="Search orders..."
        />
      </div>

      <p>{totalCount} orders</p>

      {orders.map(order => (
        <Link key={order.id} to="/orders/$orderId" params={{ orderId: order.id }}>
          <div>
            <span>#{order.id.slice(-8)}</span>
            <span>{order.status}</span>
            <span>${(order.totalCents / 100).toFixed(2)}</span>
          </div>
        </Link>
      ))}

      <div className="pagination">
        {page > 1 && (
          <button onClick={() => navigate({ search: s => ({ ...s, page: page - 1 }) })}>
            Previous
          </button>
        )}
        {page < pageCount && (
          <button onClick={() => navigate({ search: s => ({ ...s, page: page + 1 }) })}>
            Next
          </button>
        )}
      </div>
    </div>
  )
}
// src/routes/_auth/orders/$orderId.tsx — dynamic order detail route
import { createFileRoute, useLoaderData, notFound } from "@tanstack/react-router"
import { fetchOrder } from "@/api/orders"

export const Route = createFileRoute("/_auth/orders/$orderId")({
  loader: async ({ params: { orderId }, context }) => {
    const order = await context.queryClient.ensureQueryData({
      queryKey: ["orders", orderId],
      queryFn: () => fetchOrder(orderId),
    })

    if (!order) throw notFound()

    return order
  },

  // Custom not found component
  notFoundComponent: () => <p>Order not found</p>,

  component: OrderDetailPage,
})

function OrderDetailPage() {
  const order = useLoaderData({ strict: false })
  // order.id, order.status, etc. — all typed from fetchOrder return type

  return (
    <div>
      <h1>Order #{order.id.slice(-8)}</h1>
      <p>Status: {order.status}</p>
      <p>Total: ${(order.totalCents / 100).toFixed(2)}</p>
      <ul>
        {order.items.map(item => (
          <li key={item.productId}>
            {item.name} × {item.quantity} = ${((item.priceCents * item.quantity) / 100).toFixed(2)}
          </li>
        ))}
      </ul>
    </div>
  )
}

Router Setup with Context

// src/main.tsx — router with context providers
import { createRouter, RouterProvider } from "@tanstack/react-router"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { routeTree } from "./routeTree.gen"  // Auto-generated
import { useAuthStore } from "./stores/auth"

const queryClient = new QueryClient({
  defaultOptions: { queries: { staleTime: 30_000 } },
})

const router = createRouter({
  routeTree,
  defaultPreload: "intent",  // Preload on hover
  defaultPreloadStaleTime: 0,
  context: {
    queryClient,
    auth: { isAuthenticated: false, userId: null },  // Updated by provider
  },
})

// Register router instance type for useRouter() type inference
declare module "@tanstack/react-router" {
  interface Register { router: typeof router }
}

function App() {
  const auth = useAuthStore()

  return (
    <QueryClientProvider client={queryClient}>
      <RouterProvider
        router={router}
        context={{
          queryClient,
          auth: { isAuthenticated: !!auth.userId, userId: auth.userId },
        }}
      />
    </QueryClientProvider>
  )
}

For the React Router v7 (formerly Remix) alternative that offers similar file-based routing with server actions and loader patterns with SSR support baked in, see the Remix guide for nested route patterns. For the Next.js App Router that provides a different file-based approach with React Server Components and server actions instead of client-side loaders, see the Next.js guide for RSC patterns. The Claude Skills 360 bundle includes TanStack Router skill sets covering file-based routes, loaders, and search params. Start with the free tier to try TanStack Router configuration generation.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free