tanstack start
via PatrickJS/awesome-cursorrules
Full-stack React framework with server functions, streaming, and end-to-end type safety via TanStack Router and Vinxi.
What is tanstack start?
TanStack Start is a file-based routing framework built on TanStack Router and Vinxi that enables server functions, API routes, streaming with defer(), and SSR. Use it when you need a full-stack React application with type-safe server-client communication and progressive rendering via Suspense.
- Define server functions with createServerFn for type-safe server-side logic with Zod validation
- Stream non-critical data using defer() and Suspense for progressive rendering
- Create API routes with createAPIFileRoute for webhooks and third-party integrations
- Access request context (cookies, headers) in server functions for authentication
- Deploy to multiple targets (Node.js, Vercel, Netlify, Bun, Cloudflare) via app.config.ts preset
- Integrate TanStack Query for loader-level data prefetching and cache management
Applies to
File patterns this rule matches.
Rule definition (reference)
Source of truth, from the repository.
You are an expert in TanStack Start, TanStack Router, React, TypeScript, Vinxi, and full-stack type-safe web applications.
TanStack Start Guidelines
What is TanStack Start
TanStack Start is a full-stack React framework built on top of TanStack Router and Vinxi (Vite + Nitro). It provides SSR, streaming, server functions, and API routes with end-to-end type safety.
Core Principles
- TanStack Start is file-based routing via TanStack Router — all routing conventions apply
- Server Functions (
createServerFn) are the primary way to run server-side logic - Full-stack type safety: server function inputs/outputs are typed end-to-end
- Streaming and Suspense are first-class — use them for progressive rendering
- Start is NOT an API-first framework — server functions replace REST endpoints for most use cases
Project Structure
src/
routes/
__root.tsx ← Root layout with HTML shell
index.tsx ← Home route
posts/
index.tsx
$postId.tsx
server/
functions/ ← Server functions (recommended organization)
posts.ts
auth.ts
lib/
db.ts ← Database client
auth.ts ← Auth utilities
app.config.ts ← TanStack Start / Vinxi config
app.config.ts
import { defineConfig } from '@tanstack/start/config'
import tsConfigPaths from 'vite-tsconfig-paths'
export default defineConfig({
vite: {
plugins: [tsConfigPaths()],
},
server: {
preset: 'node-server', // or 'vercel', 'netlify', 'bun', 'cloudflare-pages'
},
})
Root Route Setup
// src/routes/__root.tsx
import { createRootRoute, ScrollRestoration, Scripts, Outlet } from '@tanstack/react-router'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
export const Route = createRootRoute({
component: RootComponent,
})
function RootComponent() {
return (
<html lang="en">
<head />
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
{process.env.NODE_ENV === 'development' && (
<>
<TanStackRouterDevtools />
<ReactQueryDevtools />
</>
)}
</body>
</html>
)
}
Server Functions
- Use
createServerFnto define functions that always run on the server - Validate inputs with Zod using
.validator() - Use
.handler()for the implementation - Server functions are called like regular async functions from components or loaders
// src/server/functions/posts.ts
import { createServerFn } from '@tanstack/start'
import { z } from 'zod'
export const getPost = createServerFn()
.validator(z.object({ id: z.string() }))
.handler(async ({ data }) => {
const post = await db.post.findUnique({ where: { id: data.id } })
if (!post) throw new Error('Post not found')
return post
})
export const createPost = createServerFn()
.validator(z.object({ title: z.string().min(1), body: z.string() }))
.handler(async ({ data, context }) => {
// context has access to request headers, cookies, etc.
return db.post.create({ data })
})
Using Server Functions in Routes
// src/routes/posts/$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
import { getPost } from '../../server/functions/posts'
export const Route = createFileRoute('/posts/$postId')({
loader: ({ params }) => getPost({ data: { id: params.postId } }),
component: PostDetail,
})
function PostDetail() {
const post = Route.useLoaderData()
return <article><h1>{post.title}</h1></article>
}
Mutations with Server Functions
- Call server functions directly in event handlers or via TanStack Query mutations
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { createPost } from '../../server/functions/posts'
function CreatePostForm() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (input: { title: string; body: string }) =>
createPost({ data: input }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] })
},
})
return (
<form onSubmit={(e) => {
e.preventDefault()
const fd = new FormData(e.currentTarget)
mutation.mutate({ title: fd.get('title') as string, body: fd.get('body') as string })
}}>
<input name="title" />
<textarea name="body" />
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Creating...' : 'Create'}
</button>
</form>
)
}
API Routes
- Use
createAPIFileRoutefor raw HTTP endpoints (webhooks, third-party integrations) - Place in
src/routes/api/directory
// src/routes/api/webhook.ts
import { createAPIFileRoute } from '@tanstack/start/api'
export const Route = createAPIFileRoute('/api/webhook')({
POST: async ({ request }) => {
const body = await request.json()
// handle webhook
return Response.json({ received: true })
},
})
Streaming & Suspense
- Use
defer()to stream non-critical data after the initial render - Wrap deferred data consumers in
<Suspense>
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await getPost({ data: { id: params.postId } }) // awaited (critical)
const comments = getComments({ data: { postId: params.postId } }) // not awaited (deferred)
return { post, comments: defer(comments) }
},
component: PostDetail,
})
function PostDetail() {
const { post, comments } = Route.useLoaderData()
return (
<div>
<h1>{post.title}</h1>
<Suspense fallback={<CommentsSkeleton />}>
<Await promise={comments}>
{(resolved) => <CommentsList comments={resolved} />}
</Await>
</Suspense>
</div>
)
}
Authentication
- Read cookies/headers in server functions using TanStack Start's server context
- Use
beforeLoadin routes for auth guards
import { getWebRequest } from '@tanstack/start/server'
export const getSession = createServerFn().handler(async () => {
const request = getWebRequest()
const sessionToken = getCookie(request, 'session')
return validateSession(sessionToken)
})
Deployment Targets
node-server— default Node.js serververcel— Vercel serverless/edgenetlify— Netlify Functionsbun— Bun runtimecloudflare-pages— Cloudflare Pages + Workers- Configure in
app.config.tsunderserver.preset
Environment Variables
- Access server-only vars directly from
process.envinside server functions - Use Vite's
import.meta.envfor client-exposed variables (prefix withVITE_) - Never access
process.envin client components
TanStack Query Integration
- Provide
QueryClientvia router context for loader-level prefetching - Use
ensureQueryDatain loaders to populate cache before render - This eliminates loading states for route-level data fetching
Related rules
Senior full-stack TypeScript, React, Node.js guidance with clean architecture, testing, and WHY-oriented reasoning.
Quantitative factor research skills for designing, evaluating, and mining alpha factors in equities markets.
Android development with Jetpack Compose, clean architecture, and Material Design 3.
Angular development with Novo Elements UI library using standalone components.
Expert Angular 18 + TypeScript development with Jest, emphasizing clean code and performance.
Manage Kubernetes clusters, add-ons, stacks, and credentials via the Ankra CLI platform.