tanstack start
via PatrickJS/awesome-cursorrules
Full-stack React framework with server functions, streaming, and end-to-end type safety.
What is tanstack start?
TanStack Start is a file-based routing framework built on TanStack Router and Vinxi that provides server functions, API routes, SSR, and streaming with Suspense. Use it when building full-stack React applications that need type-safe server-client communication and progressive rendering.
- Define server functions with `createServerFn` for type-safe server-side logic callable from components
- Stream non-critical data with `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 and session management
- Deploy to multiple targets (Node.js, Vercel, Netlify, Bun, Cloudflare) via `app.config.ts`
- 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
Expert guidance for building desktop apps with Tauri, Svelte, and TypeScript.
Best practices and conventions for building Temporal.io workflows and activities in Python.
TensorFlow and deep learning best practices for building, training, and deploying neural networks
Create standardized TestRail test cases with clear structure, preconditions, and expected results.
Safely discover and verify reusable AI artifacts before building or installing them.
Disciplined, quiet design system with restrained color, clear hierarchy, and accessibility-first approach.
