关于 tanstack-start
tanstack-start 是面向 TanStack Start 项目的开发技能,涵盖路由、服务端函数与部署模式。它强调加载器同时在服务端和客户端运行,以及通过类型安全边界处理敏感逻辑。
主要功能
- 01文件式路由与类型安全定义
- 02类型安全服务端函数与中间件
- 03区分同构与仅服务端代码
- 04API 路由及 TanStack Query 用法
- 05部署配置与故障排查
适用场景
- 01搭建 TanStack Start 应用路由
- 02安全处理数据库和环境变量访问
- 03为服务端函数添加认证中间件
- 04排查 Vercel 部署后的 404
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ joelhooks/ joelclaw --skill tanstack-start安装后在 Claude Code 中输入/tanstack-start确认已加载。
详细介绍
TanStack Start
基于 TanStack Router + Vite 构建的全栈 React 框架。以客户端为先,并可选择启用服务端能力。从路由到服务端函数均具备类型安全性。
适用场景
触发词:tanstack start、tanstack router、server function、createServerFn、createFileRoute、gremlin-cms、tanstack app,或任何 TanStack Start 项目相关工作。
核心概念
执行模型 — 至关重要
路由加载器是同构的——它们会在服务端和客户端都运行。这是最容易踩的坑。
// ❌ WRONG — loader runs on client too, exposes secrets
export const Route = createFileRoute('/users')({
loader: () => {
const secret = process.env.SECRET // Exposed to client!
return fetch(`/api/users?key=${secret}`)
},
})
// ✅ CORRECT — server function wraps server-only logic
const getUsers = createServerFn().handler(() => {
const secret = process.env.SECRET // Server-only
return fetch(`/api/users?key=${secret}`)
})
export const Route = createFileRoute('/users')({
loader: () => getUsers(), // Isomorphic call to server function
})
服务端函数
类型安全的 RPC,可替代 REST/tRPC/GraphQL,用于内部数据访问。构建过程会将服务端实现替换为客户端包中的 RPC 存根。
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
// GET (default)
export const getPosts = createServerFn().handler(async () => {
return db.posts.findMany()
})
// POST with input validation
export const createPost = createServerFn({ method: 'POST' })
.inputValidator(z.object({ title: z.string().min(1), body: z.string() }))
.handler(async ({ data }) => {
return db.posts.create(data)
})
架构会验证运行时输入;带有恒等函数的 TypeScript 注解则不会。请根据项目锁定的 TanStack 版本选择相应的验证器 API。
调用服务端函数的位置:
- 路由加载器 — 获取数据
- 组件 — 通过
useServerFn()Hook - 其他服务端函数 — 组合服务端逻辑
- 事件处理器 — 表单提交、点击操作
仅限服务端的函数
对于绝不能进入客户端包的工具函数:
import { createServerOnlyFn } from '@tanstack/react-start'
const getDbUrl = createServerOnlyFn(() => process.env.DATABASE_URL)
// Calling from client THROWS — crashes intentionally
基于文件的路由
app/
├── routes/
│ ├── __root.tsx # Root layout
│ ├── index.tsx # /
│ ├── about.tsx # /about
│ ├── posts/
│ │ ├── index.tsx # /posts
│ │ └── $postId.tsx # /posts/:postId
│ └── _authed/
│ └── dashboard.tsx # /dashboard (with auth layout)
├── client.tsx # Client entry
├── router.tsx # Router config
└── ssr.tsx # SSR entry
$param= 动态片段_prefix= 无路径布局路由(对路由分组,但不添加 URL 片段)__root.tsx= 根布局(包裹所有内容)
路由定义
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
// Loader runs before render (isomorphic!)
loader: ({ params }) => getPost({ data: params.postId }),
// Component receives loader data
component: PostPage,
// Error boundary
errorComponent: ({ error }) => <div>Error: {error.message}</div>,
// Pending component (while loader runs)
pendingComponent: () => <div>Loading...</div>,
})
function PostPage() {
const post = Route.useLoaderData()
return <h1>{post.title}</h1>
}
中间件
组合可复用的服务端函数中间件:
import { createMiddleware } from '@tanstack/react-start'
const authMiddleware = createMiddleware({ type: 'function' }).server(
async ({ next }) => {
const session = await getSessionFn()
if (!session?.user) throw new Error('Unauthorized')
return next({ context: { session } })
}
)
// Use in server functions
export const listPosts = createServerFn({ method: 'GET' })
.middleware([authMiddleware])
.handler(async ({ context }) => {
return db.posts.where({ userId: context.session.user.id })
})
服务端路由(API 端点)
export const Route = createFileRoute('/api/health')({
server: {
handlers: ({ createHandlers }) => createHandlers({
GET: async ({ request }) => {
return new Response(JSON.stringify({ ok: true }), {
headers: { 'Content-Type': 'application/json' },
})
},
}),
},
})
与 TanStack Query 搭配使用
import { useServerFn } from '@tanstack/react-start'
import { useQuery, useMutation } from '@tanstack/react-query'
function PostList() {
const getPostsFn = useServerFn(getPosts)
const createPostFn = useServerFn(createPost)
const { data } = useQuery({
queryKey: ['posts'],
queryFn: () => getPostsFn(),
})
const mutation = useMutation({
mutationFn: (data) => createPostFn({ data }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }),
})
}
文件组织(大型应用)
src/utils/
├── users.functions.ts # createServerFn wrappers (safe to import anywhere)
├── users.server.ts # Server-only helpers (DB queries, internal logic)
└── schemas.ts # Shared validation schemas (client-safe)
.functions.ts— 服务端函数包装器,可在任何位置安全导入.server.ts— 仅限服务端的辅助函数,绝不要从客户端代码导入
应用配置
// app.config.ts
import { defineConfig } from '@tanstack/react-start/config'
import tsConfigPaths from 'vite-tsconfig-paths'
export default defineConfig({
vite: {
plugins: [tsConfigPaths({ projects: ['./tsconfig.json'] })],
},
})
BetterAuth 集成
// Server function for session
import { createServerFn } from '@tanstack/react-start'
import { getRequestHeaders } from '@tanstack/react-start/server'
import { auth } from '@/lib/auth'
export const getSessionFn = createServerFn({ method: 'GET' }).handler(
async () => {
const headers = getRequestHeaders()
return auth.api.getSession({ headers })
}
)
// Protected layout route
export const Route = createFileRoute('/_authed')({
beforeLoad: async () => {
const session = await getSessionFn()
if (!session?.user) throw redirect({ to: '/sign-in' })
},
component: () => <Outlet />,
})
部署
TanStack Start 可部署到任何 Node/Bun 目标、Vercel、Cloudflare Workers、Netlify。
Vercel(至关重要)
必须添加 nitro() Vite 插件——没有它,Vercel 构建虽能成功,但会返回 404。
// vite.config.ts
import { nitro } from 'nitro/vite'
export default defineConfig({
plugins: [
tanstackStart(),
nitro(), // ← REQUIRED for Vercel
viteReact(),
],
})
安装:pnpm add nitro
Monorepo(pnpm + Turborepo)注意事项:
- 在 Vercel 项目设置中将
rootDirectory设为应用目录(例如apps/gremlin-cms) - 框架预设应为 TanStack Start 或自动检测,绝不能选 Next.js
- 不要手动设置
outputDirectory——Nitro 会自动生成.vercel/output - 从仓库根目录运行构建命令:
turbo run build --filter=<app-name>,或让 Vercel 自动检测 - 如果构建成功后仍出现 404,请检查:(1) 是否存在 nitro 插件,(2) 框架预设是否正确,(3) 是否存在过期的
.vercel配置
不要通过 CLI 部署——推送到 git,让 Vercel 自动部署。仅在紧急修复时使用 vercel --prod。
规则
- 绝不要在加载器中直接访问
process.env——请使用createServerFn或createServerOnlyFn - 加载器是同构的——导航期间,它们会在服务端和客户端都运行
- 服务端函数是边界——任何涉及数据库、环境变量或机密的操作都必须通过
createServerFn - 内部数据访问优先使用服务端函数,而非 API 路由——类型安全,无需手动 fetch
- 从组件调用服务端函数时使用
useServerFn()Hook(不要直接调用) - 中间件可以组合——将身份验证、验证、日志记录作为可复用中间件叠加
持续更新的文档
随着我们构建 gremlin-cms,此技能将不断完善。请根据开发期间发现的模式进行更新。