
Tanstack Start
- 142 installs
- 202 repo stars
- Updated August 4, 2026
- secondsky/claude-skills
Helps with ai & agent building tasks during AI-assisted development.
About
tanstack-start is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- tanstack-start
- AI & Agent Building
- AI-coding skill
Tanstack Start by the numbers
- 142 all-time installs (skills.sh)
- +11 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #3,455 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/secondsky/claude-skills --skill tanstack-startAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 142 |
|---|---|
| repo stars | ★ 202 |
| Last updated | August 4, 2026 |
| Repository | secondsky/claude-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
TanStack Start (React) — RC-Ready Playbook
Full-stack React on TanStack Router with per-route SSR/CSR, file-based routing, server functions, and first-class Cloudflare Workers support.
Use this skill when
- Building a greenfield React app that needs route-level SSR/CSR/SSG switches.
- Migrating from Next.js/React Router while keeping file-based routing + API routes.
- Shipping to edge runtimes (Workers) with typed server functions and bindings.
- You want predictable routing with type-safe params/search + built-in preloading.
What’s inside
- References: quickstart/layout, rendering modes, server functions, Cloudflare hosting, execution/auth, plus new routing/data/navigation/devtools guides.
- Script:
scripts/bootstrap-cloudflare-start.sh <app>scaffolds Start + Workers + binding types. - Troubleshooting: hydration, API routing, bindings, navigation/preloading failures.
---
Quick Start (React)
npm create @tanstack/start@latest my-app
cd my-app
npm run devManual installs (all bundle targets are supported): add @tanstack/react-router + @tanstack/react-start with your bundler plugin (vite, webpack, or esbuild) per the official install guides.
Core layout reminder
app/routes/**file-based routes → router tree, automatic code-splitting + data preloading.app/entry.client.tsxhydrates<StartClient />;app/entry.server.tsxwrapscreateServerEntry.app/config.tsorapp/start.tssetsdefaultSsr,spaMode, middleware, and context.
---
Routing + Data Best Practices
- Type-safe params & search:
createFileRoute()infers path params; addvalidateSearch(zod) to parse and coerce search params. - Route matching order is deterministic (index → static → dynamic → splat); rely on this when adding catch-alls.
- Loaders run once per location change; return plain data, throw
redirect()/notFound()for control flow. - Data mutations: colocate
action/server functions; keep loaders read-only and invalidate viarouter.invalidate()after mutation. - TanStack Query bridge: create a
QueryClientin router context andensureQueryDatainside loaders to dedupe fetches. - Deferred/external data: stream partial data or read from external loaders; prefer suspense-friendly responses.
- Head management: set
headper route for<title>/meta; derive from loader data to keep SEO consistent. - Not-found/auth: throw
notFound()orredirect()in loaders/middleware; use error boundaries for UX.
Example route (typed search + data-only SSR):
// app/routes/posts.$postId.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'
import { z } from 'zod'
export const Route = createFileRoute('/posts/$postId')({
validateSearch: z.object({ preview: z.boolean().optional() }),
ssr: 'data-only',
loader: async ({ params, search, context }) => {
const post = await context.queryClient.ensureQueryData(['post', params.postId], () =>
fetch(`/api/posts/${params.postId}?preview=${!!search.preview}`).then(r => r.json())
)
if (!post.published && !search.preview) throw redirect({ to: '/drafts' })
return { post }
},
})---
Navigation, Preloading, and UX
- Link prefetch defaults:
<Link preload="intent">(hover/focus) preloads route data/code; usepreload="render"for above-the-fold routes. - Programmatic preloading:
router.preloadRoute({ to, search })to warm caches before navigation (e.g., on visibility). - Route masking: keep canonical URLs while showing user-friendly masks (e.g.,
/products?slug=abcmasked as/p/abc). - Navigation blocking: protect unsaved forms with
router.navigate({ to, replace, from })blockers oruseBlocker. - Scroll restoration: enable
scrollRestorationto restore positions on back/forward; customize per route when using long lists. - Search param serialization: customize parse/stringify to keep numbers/dates stable and avoid stringified booleans.
---
Rendering & Performance
- Per-route SSR: set
ssr: true | false | 'data-only'on routes;defaultSsrconfig sets the baseline. - Code-splitting: file-based routes auto-split; add
lazy/loadfor manual chunks on code-based routes. - Preloading strategy: pair
preload="intent"links withdefaultPreloadStaleTimeto avoid over-fetching. - Render optimizations: keep loaders pure, memoize heavy components, and use
pendingComponentfor CSR routes to avoid layout shift.
---
Devtools, Linting, and LLM Support
- Add
<RouterDevtools />during development to inspect matches, loader states, and preloading. - Enable the ESLint plugin
@tanstack/eslint-plugin-routerwith the recommended config to enforce inference-sensitive property order (e.g.,beforeLoadbeforeloader). - LLM-aware routing: the Router exposes structured route metadata to LLM agents; keep descriptions concise in
Routemeta for better AI navigation.
---
Deployment Notes (Cloudflare-friendly)
- Keep
cloudflare({ viteEnvironment: { name: 'ssr' } })first in Vite plugins so bindings reach server entry. - Regenerate bindings after changes:
npm run cf-typegen. - For static-heavy sites, enable prerender to ship HTML to Workers Assets/Pages; exclude param routes or add explicit
pages.
---
Ship Checklist
- [ ] Routes load without hydration warnings (prefer
ssr: 'data-only'for non-deterministic UI). - [ ] Search params validated with
validateSearchand custom serializer where needed. - [ ] Link preloading configured for high-traffic routes; blockers added for unsaved forms.
- [ ] ESLint plugin enabled (
create-route-property-orderrule) andnpm run checkpasses. - [ ] Devtools verified locally;
router.matchesstate looks correct. - [ ] Cloudflare bindings typed (
cf-typegen) and streaming tested viacurl -N.
Cloudflare Hosting + Environment Management
Workers Setup (vite-plugin)
// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import { cloudflare } from '@cloudflare/vite-plugin'
import viteReact from '@vitejs/plugin-react'
export default defineConfig({
plugins: [
cloudflare({ viteEnvironment: { name: 'ssr' } }),
tanstackStart(),
viteReact(),
],
})// wrangler.jsonc
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "tanstack-start-app",
"compatibility_date": "2025-09-02",
"compatibility_flags": ["nodejs_compat"],
"main": "@tanstack/react-start/server-entry",
"observability": { "enabled": true }
}Package scripts (Cloudflare docs recommendation):
{
"scripts": {
"dev": "vite dev",
"build": "vite build && tsc --noEmit",
"preview": "vite preview",
"deploy": "npm run build && wrangler deploy",
"cf-typegen": "wrangler types"
}
}Bindings
- Declare KV/R2/D1/Secrets in
wrangler.jsonc(kv_namespaces,d1_databases,r2_buckets,vars). - Start exposes bindings to server handlers via the
envargument when usingcreateStartHandleror in thecontextpassed to server functions/routes. Keep client code onimport.meta.env.
Environment Variables Rules
- Server: use
process.env.SECRET_NAMEorenv.SECRET_NAME(Workers bindings). No prefix required. - Client: only
import.meta.env.VITE_*is available; anything else is stripped at build. - Load order:
.env.local→.env.production/.env.development→.env. - Add
src/env.d.tsto typeimport.meta.envand prevent typos.
Tailwind CSS (v4) + Paths
// vite.config.ts (Tailwind v4)
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
cloudflare({ viteEnvironment: { name: 'ssr' } }),
tanstackStart(),
viteReact(),
tailwindcss(),
],
})- Create
app/styles/app.csswith@import 'tailwindcss';and link in__root.tsxusing?url. - Enable TS path aliases with
vite-tsconfig-pathsto keep imports short (@/lib/db). Add topluginsandtsconfig.jsonpaths.
Deployment Checklist
wrangler loginonce per machine; CI usesCLOUDFLARE_API_TOKEN.npm run cf-typegenafter adding bindings to keep types in sync.- For streaming/server functions on Workers, keep
cloudflare({ viteEnvironment: { name: 'ssr' } })first in plugins so fetch handler binds correctly. - If using prerender + static server functions, ensure KV/R2 assets bucket is configured to serve generated JSON alongside HTML.
Devtools, DX, and LLM Support
Router Devtools
- Install
@tanstack/router-devtoolsand render<RouterDevtools />in__root.tsxduring development. - Use it to inspect route matches, loader/action states, search params, and preloading; keep it disabled in production builds.
ESLint + Property Order
- Add
@tanstack/eslint-plugin-routerwith the recommended config; it enforces the inference-sensitive property order (beforeLoad→loader→component). - Pair with
typescript-eslintstrict rules to catch missing params/search typings early.
DX Decisions
- Prefer file-based routes for co-location and automatic code-splitting; switch to code-based only when composition or dynamic route sets are required.
- Keep loaders pure and colocated; move heavy dependencies behind lazy imports to shrink critical path.
LLM Support (experimental)
- The router exposes typed route metadata that can be serialized for LLM agents; keep route descriptions concise and deterministic.
- Provide friendly names in route meta for improved grounding (e.g.,
meta: { title: 'Billing Settings' }). - Avoid dynamic titles that depend on user data unless you guard them with authentication checks to prevent leaking context.
Execution Model, Env Functions, and Auth/Data Patterns
Execution Model (what runs where)
- Route modules execute on the server for SSR, then on the client after navigation. Avoid side effects at module scope; put work in loaders/components.
- Use environment functions to fence code:
import { createServerOnlyFn, createClientOnlyFn } from '@tanstack/react-start'
export const readSecret = createServerOnlyFn(() => process.env.API_KEY!)
export const focusInput = createClientOnlyFn(() => document.querySelector('input')?.focus())- Prefer
createIsomorphicFn()when you need one call site with server/client branches (e.g., logging to console vs. sending to APM).
Authentication Building Blocks
- Sessions:
useSession()in server functions or loaders; storeSESSION_SECRETin env/binding. - Protect routes with middleware:
const requireUser = createMiddleware({ type: 'function' }).server(async ({ next }) => {
const session = await useSession()
if (!session.data.userId) throw redirect({ to: '/login' })
return next({ context: { userId: session.data.userId } })
})- Login/logout as server functions; throw
redirect()after setting/clearing session. - Hosted options with examples: Clerk, WorkOS, Auth.js, Supabase (see official examples list).
Data Layer Guidance
- Keep database clients (D1, Neon, PlanetScale, Prisma, Drizzle) in
app/lib/db.tsand import only in server functions/server routes. - For Workers, prefer edge-friendly drivers (D1 binding, Prisma Data Proxy, Postgres HTTP/Neon) to avoid TCP.
- Co-locate validation schemas with server functions to reuse on the client via zod types.
Observability
- Enable
observabilityinwrangler.jsoncfor request traces; forwardctx.waitUntil(logger.flush())in handlers. - Use
console.login server functions; Start surfaces logs in dev and Workers tail. For client, gate logs behindimport.meta.env.DEV.
Path Aliases
- Add to
tsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": { "@/*": ["./app/*"] }
}
}- Install
vite-tsconfig-pathsand add topluginsaftertanstackStart().
Navigation, Preloading, and UX
Link Options
- Use
<Link preload="intent">to preload on hover/focus; switch topreload="render"for above-the-fold routes orpreload={false}for rarely visited pages. - Pass
resetScroll/replace/targetonly when needed; defaults keep back/forward UX predictable. - Build custom links with
router.buildLink()to reuse the same preloading semantics across design systems.
Programmatic Preloading
- Warm caches before navigation:
router.preloadRoute({ to: '/products/$id', params: { id }, search: { preview: true } })- Pair with
defaultPreloadStaleTimeto avoid refetch storms when hovering multiple links quickly.
Route Masking
- Keep canonical URLs while displaying masked, user-friendly paths (
mask: { to: '/p/$id' }); masks update the address bar without changing the matched route. - Good for A/B or marketing URLs that should still resolve to canonical data paths.
Navigation Blocking
- Use
useBlockerorrouter.block()inside forms to guard unsaved changes; unblock after save or explicit discard. - Provide a confirm dialog and redirect target for failed saves to avoid dead-end states.
Scroll Restoration
- Enable
scrollRestorationglobally, then override per route for long lists (e.g., restore previous position on back/forward but scroll to top on fresh navigation). - Combine with
pendingComponentto avoid layout jumps while data loads.
Document Head
- Set
headper route using loader data for titles/meta; avoid readingwindowdirectly to keep SSR safe. - Keep head updates deterministic (no
Date.now()), or move non-deterministic pieces behindssr: 'data-only'routes.
TanStack Start Quickstart & Layout
Scaffold Commands
npm create @tanstack/start@latest my-app— official CLI (React + Vite).npm create cloudflare@latest -- --framework=tanstack-start my-app— preconfigures Cloudflare Workers + wrangler.npm installthennpm run dev(port 3000 by default),npm run build,npm run preview.- Manual installs (when you already have a bundler):
- Vite:
npm i @tanstack/react-router @tanstack/react-start @tanstack/router-plugin - Webpack:
npm i @tanstack/react-router @tanstack/react-start @tanstack/router-plugin - Esbuild/Rspack:
npm i @tanstack/react-router @tanstack/react-start @tanstack/router-plugin
Add the matching plugin entry point (@tanstack/router-plugin/vite|webpack|esbuild|rspack) to your bundler config and call createRouter() / createStart() in your entry.
Generated File Map (React)
app/config.ts(orapp/start.tsin newer templates) — callscreateStartand wires router + defaults (e.g.,defaultSsr).app/routes/— file-based routes (__root.tsx,index.tsx,posts/$postId.tsx, etc.).app/entry.client.tsx— hydrates with<StartClient />viahydrateRoot.app/entry.server.tsx— wraps the universal fetch handler withcreateServerEntry.app/server-functions/(optional) — colocatecreateServerFnhandlers you import into routes/components.public/— static assets served as-is.
Minimal Route Example
// app/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
loader: async () => ({ now: new Date().toISOString() }),
component: () => {
const { now } = Route.useLoaderData()
return <h1 className="text-xl font-semibold">Hello from Start — {now}</h1>
},
})Entry Points Cheat Sheet
- Client (
app/entry.client.tsx):
import { StartClient } from '@tanstack/react-start/client'
import { hydrateRoot } from 'react-dom/client'
import { StrictMode } from 'react'
hydrateRoot(document, <StrictMode><StartClient /></StrictMode>)- Server (
app/entry.server.tsx):
import handler from './handler'
import { createServerEntry } from '@tanstack/react-start/server'
const entry = createServerEntry({
fetch(request, opts) {
return handler.fetch(request, opts)
},
})
export default {
fetch(request: Request, env: unknown, ctx: ExecutionContext) {
return entry.fetch(request, env, ctx)
},
}Add custom middleware/headers by wrapping handler.fetch with createStartHandler when needed.
Everyday DX Tips
- Keep shared types in
app/types.tsand import in routes + server functions. - Use
tsconfig.paths+vite-tsconfig-pathsso imports stay short (@/routes/...). - Prefer
npm run check(orpnpm check) to catch route type drift before deploy.
Rendering Modes & Hydration
SSR, Data-Only, or CSR
- Default: routes render + hydrate on the server (
ssr: true). - Per-route override:
export const Route = createFileRoute('/chart')({
ssr: 'data-only', // options: true | false | 'data-only'
loader: () => getChartData(),
component: Chart,
})- Make SSR opt-in/off by default:
// app/start.ts
export const start = createStart(() => ({ defaultSsr: false }))- Use the functional form for context-aware SSR:
ssr: ({ params, search }) => search.value?.preview ? 'data-only' : true.
SPA Mode (global CSR)
Disable all SSR (loaders + components) when you want a pure SPA but keep Start’s router + server functions:
export const start = createStart(() => ({
spaMode: true,
defaultPreloadStaleTime: 0,
}))Static Prerender (SSG)
Enable Vite plugin prerendering to emit static HTML during npm run build:
// vite.config.ts
plugins: [
tanstackStart({
prerender: {
enabled: true,
autoStaticPathsDiscovery: true, // crawl links and static routes
autoSubfolderIndex: true, // /page/index.html output
},
}),
]- Exclude routes with params from auto-discovery; add them via
pagesor ensure crawlable links. - Combine with Cloudflare Workers Assets/Pages for zero-runtime hosting when no server functions are used.
Hydration Error Playbook
- Make server + client deterministic: hoist locale/time zone to cookies, avoid
Date.now()in render, seed random IDs. - Use
<ClientOnly>for inherently client-side widgets (e.g., canvas, relative time). - Prefer
ssr: 'data-only'instead of suppressing hydration when markup differs. - Only use
suppressHydrationWarningfor tiny, known-different fragments (timestamps).
Pending UI for non-SSR routes
- First route with
ssr: falseor'data-only'showspendingComponent(ordefaultPendingComponent) while the client loads. - Set
minPendingMsto avoid flash during hydration for CSR-heavy routes.
Routing & Data Loading (TanStack Start / Router)
Route Tree and File-Based Routing
- Files under
app/routes/**become routes;createFileRoute()infers params and builds the route tree automatically. Use code-based routes (createRootRoute,createRoute,rootRoute.addChildren) when you need dynamic composition. - Route matching order: index → static segments → dynamic (
$id) → splat ($), so place catch-alls last to avoid accidental matches.
Path + Search Params (Type Safe)
- Path params are typed from the file name (
/posts/$postId.tsx→{ postId: string }). - Add
validateSearchwith zod to coerce/validate search params:
export const Route = createFileRoute('/posts/')({
validateSearch: z.object({ page: z.coerce.number().default(1) }),
})- For custom serialization (dates, arrays), provide
parseSearch/stringifySearchto keep types stable across navigations.
Loaders (data fetching)
loaderruns once per location change; return plain data or throwredirect()/notFound()to control flow.- Keep loaders pure and side-effect free; mutations should live in actions or server functions.
- Access router context (
context.queryClient, user/session, env) via the loader signature.
Deferred + External Data
- When upstream latency is high, return partial data and stream the rest using deferred responses; pair with suspense boundaries in components.
- External data loaders can live outside route files; pass them into
loaderto keep route modules lean.
Mutations
- Use route
action(or Start server functions) for writes; invalidate affected routes withrouter.invalidate()or query invalidation when using TanStack Query.
export const Route = createFileRoute('/posts/new')({
action: async ({ data, context }) => {
await context.api.createPost(data)
await context.queryClient.invalidateQueries({ queryKey: ['posts'] })
return redirect({ to: '/posts' })
},
})Static Route Data
- Add
staticDatafor values that never change (e.g., breadcrumb labels); Start can inline this in prerendered builds to avoid runtime fetches.
Error & Not-Found Handling
- Throw
notFound()inside loaders/actions for 404s; render per-routenotFoundComponentor an error boundary to show friendly UX. - Prefer structured errors over generic throws so error boundaries can branch on status/type.
Server Functions, Routes & Middleware
Server Functions (RPC-style)
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
export const saveNote = createServerFn({ method: 'POST' })
.inputValidator(z.object({ body: z.string().min(1) }))
.handler(async ({ data, request }) => {
const userId = request.headers.get('x-user-id')
if (!userId) throw new Response('Unauthorized', { status: 401 })
await db.notes.insert({ userId, body: data.body })
return { ok: true }
})
// Call from client or loaders:
await saveNote({ body: 'hello' })- Runs only on the server; the client call becomes a fetch to the compiled endpoint.
- Access request with
getRequest()helpers or handler args; headers/cookies available. - Return plain data,
Response, redirects (throw redirect({ to: '/login' })), or not-found.
Static server functions (prerender-friendly)
import { staticFunctionMiddleware } from '@tanstack/start-static-server-functions'
export const cachedSettings = createServerFn()
.middleware([staticFunctionMiddleware]) // execute at build, emit static JSON
.handler(async () => fetchSettingsFromCms())Server Routes (raw HTTP)
// app/routes/api/health.ts
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/api/health')({
server: {
handlers: {
GET: () => Response.json({ ok: true, at: Date.now() }),
POST: async ({ request }) => {
const body = await request.json()
return Response.json({ echoed: body })
},
},
},
})- Lives beside page routes; supports params (
/api/users/$id.ts), splats, and route-level middleware. - Use for webhooks, form posts, or streaming responses without React render.
Middleware Patterns
import { createMiddleware, createServerFn } from '@tanstack/react-start'
import { redirect } from '@tanstack/react-router'
const requireAuth = createMiddleware({ type: 'function' })
.server(async ({ next, context }) => {
const user = await getSessionUser()
if (!user) throw redirect({ to: '/login' })
return next({ context: { user } })
})
export const updateProfile = createServerFn({ method: 'POST' })
.middleware([requireAuth])
.handler(async ({ data, context }) => {
return db.user.update(context.user.id, data)
})createMiddleware()defaults to request middleware; use{ type: 'function' }for server-function-specific flows.- Chain middleware with
.middleware([...]); alwaysreturn next()to continue. - Register global middleware in
createStart(() => ({ requestMiddleware: [...], functionMiddleware: [...] })).
Error Boundaries & Hydration Safety
- Throw
redirect()ornotFound()from loaders/server functions; Start will surface in route error boundaries. - For hydration-sensitive components, pair with
ssr: 'data-only'(see rendering-modes.md) or wrap UI in a client-only boundary.
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <app-name>" >&2
exit 1
fi
APP_NAME="$1"
echo "Scaffolding TanStack Start (React) for Cloudflare Workers into ./${APP_NAME}"
# 1) Create the project with Cloudflare wiring preconfigured.
npm create cloudflare@latest -- --framework=tanstack-start "${APP_NAME}"
cd "${APP_NAME}"
# 2) Install dependencies if the generator skipped them (idempotent).
npm install
# 3) Generate Workers binding types for safer server functions.
npm run cf-typegen || true
cat <<'EONEXT'
Next steps:
cd '"${APP_NAME}"'
npm run dev # local dev on http://localhost:3000
npm run build # production build
npm run deploy # deploy via Wrangler (requires CLOUDFLARE_API_TOKEN or wrangler login)
Optional:
- Add Tailwind v4: npm install -D @tailwindcss/vite && update vite.config.ts (see references/cloudflare-hosting-and-env.md)
- Add Query: npm install @tanstack/react-query @tanstack/react-query-devtools
- Enable SSR selectively: edit app/start.ts defaultSsr + per-route ssr flag
EONEXT