@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 Start | Next.js (App Router) | React Router v7 / Remix | |
|---|---|---|---|
| Routing | File-based, src/routes/, generated routeTree.gen.ts | File-based, app/ segment tree | File-based or config-based (react-router.config.ts) |
| Server data layer | createServerFn() — RPC-style, called directly from components | Server Components + Server Actions | loader / action exports per route |
| Type safety across the network boundary | End-to-end inferred from createServerFn + TanStack Router | Partial — Server Actions are typed, fetches to Route Handlers aren’t | Loader/action return types inferred, but not RPC-typed |
| Build tooling | Vite (or Rsbuild) | Turbopack / Webpack | Vite |
| Best fit | Teams already deep in TanStack Query/Router who want one cohesive stack with strong type inference | Teams that want the largest ecosystem, ISR/edge caching maturity, and Vercel-native deploys | Teams that want a thinner abstraction over standard web fetch/loader semantics |
| Where it struggles | Younger deployment-target documentation, smaller Stack Overflow footprint | Server Actions’ implicit POST-everywhere model surprises non-Next teams | No 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.