Claude Code TanStack Start React CLAUDE.md AI Coding TanStack Router 2026

TanStack Start Just Crossed 16M Weekly Downloads. Your CLAUDE.md Doesn't Know It Exists (2026)

The Prompt Shelf ·

@tanstack/react-start pulled roughly 16.4 million weekly npm downloads in the last week of August 2026 — for comparison, next pulled about 55.3 million and @tanstack/react-router, which Start is built on, pulled 22.1 million. That’s not “worth watching.” That’s a framework that’s already inside a meaningful share of the React codebases Claude Code touches every day, with a monorepo (TanStack/router) that had a commit land the same day we pulled these numbers.

None of that shows up in most CLAUDE.md files. We checked: the AI Coding Rules directory has zero dedicated TanStack Start entries, and the handful of TanStack Start “Claude Code skill” packages floating around GitHub are tool-configuration bundles (hooks, slash commands, agent definitions) rather than the kind of rules file a project actually loads into context. So an agent working in a TanStack Start repo has nothing project-specific to draw on, and falls back to whatever full-stack React pattern shows up most in training data — which, for the vast majority of 2023–2025 code, is Next.js App Router or Remix. Those patterns don’t just look different in TanStack Start. Several of them fail in ways that don’t throw an error.

The decision most teams skip: is this even the right framework

Before writing any CLAUDE.md rules, it’s worth stating plainly when TanStack Start earns a place over the frameworks it’s most often compared to — because half the mismatched-pattern problem below happens when a team adopts Start for real reasons but never tells their agent why, so it keeps reaching for the old framework’s idioms.

TanStack StartNext.js (App Router)React Router v7 / Remix
RoutingFile-based, src/routes/, generated routeTree.gen.tsFile-based, app/ segment treeFile-based or config-based (react-router.config.ts)
Server data layercreateServerFn() — RPC-style, called directly from componentsServer Components + Server Actionsloader / action exports per route
Type safety across the network boundaryEnd-to-end inferred from createServerFn + TanStack RouterPartial — Server Actions are typed, fetches to Route Handlers aren’tLoader/action return types inferred, but not RPC-typed
Build toolingVite (or Rsbuild)Turbopack / WebpackVite
Best fitTeams already deep in TanStack Query/Router who want one cohesive stack with strong type inferenceTeams that want the largest ecosystem, ISR/edge caching maturity, and Vercel-native deploysTeams that want a thinner abstraction over standard web fetch/loader semantics
Where it strugglesYounger deployment-target documentation, smaller Stack Overflow footprintServer Actions’ implicit POST-everywhere model surprises non-Next teamsNo RPC-style server functions — you’re wiring loaders/actions by hand

If your project already reads from this, TanStack Query for server cache, and TanStack Router for routing, and you want a server layer that shares the same type system instead of bolting on a separate Server Actions or loader convention, Start is the coherent choice. If you need Vercel’s ISR/edge caching maturity or the largest hiring pool, Next.js still wins on those axes specifically. Our React Router v7 and Remix 3 piece covers the third leg of this comparison in more depth. The Next.js + TypeScript and Remix + Supabase rule sets in our gallery are useful side-by-side reading if your team is choosing between all three.

Mistake 1: an agent “fixes” a routing bug by editing routeTree.gen.ts

TanStack Start generates src/routeTree.gen.ts automatically every time the dev server or build runs, from the files in src/routes/. It’s not a scaffold you fill in once — it’s regenerated on every relevant file change, and the official docs are explicit that the route paths inside it are “automatically written and managed by the router for you via the TanStack Router Bundler Plugin or Router CLI.” An agent that sees a routing type error and doesn’t know this file is generated will do exactly what it would do with a hand-maintained next.config.js route entry: open the file and edit the string directly.

// ❌ Editing the generated file — overwritten on the next dev server restart,
// and the fix silently disappears with no error
// src/routeTree.gen.ts
const PostsPostIdRoute = PostsRoute.addChildren([
  createRoute({ path: '/posts/:postId' as any, ... }) // hand-patched
])
// ✅ Fix the source file that generates the route
// src/routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({ ... })

The naming convention itself is the other half of this trap: posts/$postId.tsx produces /posts/:postId, and rest/$.tsx produces a wildcard. Every one of those $ prefixes reads as a shell variable or a typo to a model trained mostly on Next.js’s [param] and Remix’s $param (no leading $ on the segment name) conventions — close enough to cause silent misreads, different enough to break.

Mistake 2: server functions get written like Next.js Route Handlers

createServerFn() isn’t a REST endpoint you fetch — it’s a same-origin RPC call. The build process replaces the server implementation with a typed network stub in the client bundle, so the calling code looks like a normal async function call, not a fetch('/api/...'). An agent carrying over Next.js Route Handler muscle memory tends to build a /api/* file structure and a matching fetch wrapper that TanStack Start doesn’t need and won’t type-check as cleanly:

// ❌ Next.js habit: hand-rolled API route + fetch wrapper, loses RPC type inference
// app/api/users/route.ts
export async function POST(req: Request) { /* ... */ }
// client
await fetch('/api/users', { method: 'POST', body: JSON.stringify(data) })
// ✅ TanStack Start: createServerFn is called directly, typed end-to-end
export const createUser = createServerFn({ method: 'POST' })
  .validator(UserSchema)
  .handler(async ({ data }) => {
    return db.user.create({ data })
  })

// client component
const user = await createUser({ data: { name: 'Ada' } })

The .validator() call matters beyond input sanity-checking: strict: true is the default, so a non-serializable return type fails at build time, not runtime. If your CLAUDE.md tells Claude Code to “return the full ORM model from the handler,” that rule breaks the moment the model includes something non-serializable (a class instance, a circular reference, a Date without a custom serializer) — a rule written for a Next.js Server Action doesn’t carry the same risk because Server Actions serialize more permissively.

Mistake 3: beforeLoad gets treated as the auth boundary — it isn’t

This is the gap most likely to ship a real vulnerability, not just a bug. TanStack Start’s route-level beforeLoad guard is a UX affordance — it keeps an unauthenticated user from seeing a flash of protected UI before redirecting. It is not a security boundary, because a server function can still be called directly (by URL, by a script, by a replayed request) without ever going through the route’s beforeLoad. The official guidance states this outright: a route guard “improves route UX, but it is not the data boundary.” An agent that only knows Next.js middleware-based auth (where the middleware genuinely does sit in front of every request) will assume the equivalent route guard here does the same job. It doesn’t.

// ❌ Auth only at the route level — the server function itself has no protection
export const Route = createFileRoute('/dashboard')({
  beforeLoad: ({ context }) => {
    if (!context.user) throw redirect({ to: '/login' })
  },
})
// getDashboardData is still callable directly with no auth check at all
export const getDashboardData = createServerFn().handler(async () => db.dashboard.find())
// ✅ Auth middleware attached to the server function itself
const authMiddleware = createMiddleware({ type: 'function' }).server(async ({ next, context }) => {
  const session = await getSession()
  if (!session) throw new Error('Unauthorized')
  return next({ context: { user: session.user } })
})

export const getDashboardData = createServerFn()
  .middleware([authMiddleware])
  .handler(async ({ context }) => db.dashboard.find({ userId: context.user.id }))

The rule for CLAUDE.md to state explicitly: every createServerFn that reads or writes private data gets its own authMiddleware, independent of whatever beforeLoad guard sits on the route that calls it. Route guards and data boundaries are two separate walls, not one.

Mistake 4: client-sent context gets trusted after one validation pass

Middleware in TanStack Start can pass data from client to server via sendContext, and the docs are specific about the failure mode here: validating the shape of that data (it’s a UUID, it’s a string) is not the same as validating authorization (this session is allowed to access this resource). An agent that adds a Zod schema and calls it done has covered exactly one of the two checks the docs call out by name.

// ❌ Shape-validated, not authorization-checked
.validator(z.object({ projectId: z.string().uuid() }))
.handler(async ({ data, context }) => {
  return db.project.findUnique({ where: { id: data.projectId } }) // any valid UUID works
})
// ✅ Shape, then ownership
.validator(z.object({ projectId: z.string().uuid() }))
.handler(async ({ data, context }) => {
  const project = await db.project.findUnique({ where: { id: data.projectId } })
  if (project?.ownerId !== context.user.id) throw new Error('Forbidden')
  return project
})

Mistake 5: a server function gets imported dynamically

Static imports of server functions are safe — the build’s tree-shaking strips the server-only implementation out of the client bundle and leaves the RPC stub behind. Dynamic import() breaks that mechanism, because the bundler can’t statically determine at build time which module boundary to split. The official docs list this as a named gotcha, not an edge case: “avoid dynamic imports for server functions.” It’s an easy one for an agent to introduce by accident when it’s applying a general “lazy-load anything not needed on first render” instinct that’s a perfectly good rule everywhere else in a React codebase.

The CLAUDE.md block

This is the rule set that closes the gaps above — short enough to actually get read, specific enough to stop the five mistakes:

## TanStack Start

- Routing lives in `src/routes/`. NEVER hand-edit `src/routeTree.gen.ts`
  it's regenerated on every dev server run and build. Fix the source route
  file, not the generated tree.
- File naming: `$paramName.tsx` = dynamic segment, `$.tsx` = wildcard,
  `index.tsx` = index route, `__root.tsx` = root layout (no path).
- Server-side logic goes in `createServerFn()`, not a hand-rolled `/api/*`
  route + `fetch()` wrapper. It's an RPC call, not a REST endpoint.
- Every `createServerFn` that reads or writes private data MUST have its
  own `authMiddleware` (or equivalent) attached directly. A route-level
  `beforeLoad` guard is UX only — it does not protect the server function
  from being called directly.
- Client-sent context passed via `sendContext` must be validated twice:
  shape (is it a well-formed UUID/string/etc.) AND authorization (does
  this session own/have access to this resource). Shape validation alone
  is not a security check.
- Server functions must use static imports only. Dynamic `import()` of a
  server function breaks the client/server code-splitting boundary.
- `strict: true` is the default on `createServerFn` — return values must
  be serializable. Don't return raw ORM model instances if they carry
  non-serializable fields; map to a plain object first.

Drop this alongside whatever stack-specific rules your project already has — Drizzle, tRPC, or Convex CLAUDE.md sections layer on top of this without conflict, since none of them touch routing or the server-function boundary directly.

Browse more verified CLAUDE.md and AGENTS.md examples in our gallery, including full-stack React setups you can compare against your own stack.

Related Articles

Explore the collection

Browse all AI coding rules — CLAUDE.md, .cursorrules, AGENTS.md, and more.

Browse Rules