mcpField
T
tanstack-start

tanstack-start

github.com/joelhooks开发6537 次安装2 天前更新
代码仓库

关于 tanstack-start

tanstack-start 是面向 TanStack Start 项目的开发技能,涵盖路由、服务端函数与部署模式。它强调加载器同时在服务端和客户端运行,以及通过类型安全边界处理敏感逻辑。

主要功能

  1. 01文件式路由与类型安全定义
  2. 02类型安全服务端函数与中间件
  3. 03区分同构与仅服务端代码
  4. 04API 路由及 TanStack Query 用法
  5. 05部署配置与故障排查

适用场景

  1. 01搭建 TanStack Start 应用路由
  2. 02安全处理数据库和环境变量访问
  3. 03为服务端函数添加认证中间件
  4. 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。

规则

  1. 绝不要在加载器中直接访问 process.env——请使用 createServerFn 或 createServerOnlyFn
  2. 加载器是同构的——导航期间,它们会在服务端和客户端都运行
  3. 服务端函数是边界——任何涉及数据库、环境变量或机密的操作都必须通过 createServerFn
  4. 内部数据访问优先使用服务端函数,而非 API 路由——类型安全,无需手动 fetch
  5. 从组件调用服务端函数时使用 useServerFn() Hook(不要直接调用)
  6. 中间件可以组合——将身份验证、验证、日志记录作为可复用中间件叠加

持续更新的文档

随着我们构建 gremlin-cms,此技能将不断完善。请根据开发期间发现的模式进行更新。

评论

支持 Markdown