关于 tanstack-start
指导 Claude 从零创建部署于 Cloudflare Workers 的 TanStack Start 全栈应用,涵盖项目结构、身份验证、D1 数据库访问、CRUD 流程、本地检查与部署。
主要功能
- 01从零生成项目文件
- 02TanStack Start SSR 与文件路由
- 03better-auth 身份验证配置
- 04D1 与 Drizzle 数据库模式
- 05CRUD 服务端函数与部署步骤
适用场景
- 01在 Cloudflare Workers 上启动全栈应用
- 02添加邮箱密码或 Google OAuth 身份验证
- 03实现经过身份验证的 CRUD 工作流
- 04检查并部署 TanStack Start 应用
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill tanstack-start安装后在 Claude Code 中输入/tanstack-start确认已加载。
详细介绍
Cloudflare 上的 TanStack Start
从零开始构建完整的全栈应用。Claude 生成每个文件——无需克隆模板,也无需运行脚手架命令。
技术栈:TanStack Start v1(SSR、基于文件的路由、通过 Nitro 实现的服务端函数),运行于 Cloudflare Workers;React 19 + Tailwind v4 + shadcn/ui;D1 + Drizzle;better-auth(Google OAuth + 邮箱/密码)。
项目文件树
PROJECT_NAME/
├── src/
│ ├── routes/
│ │ ├── __root.tsx # Root layout (HTML shell, theme, CSS import)
│ │ ├── index.tsx # Landing / auth redirect
│ │ ├── login.tsx # Login page
│ │ ├── register.tsx # Register page
│ │ ├── _authed.tsx # Auth guard layout route
│ │ ├── _authed/
│ │ │ ├── dashboard.tsx # Dashboard with stat cards
│ │ │ ├── items.tsx # Items list table
│ │ │ ├── items.$id.tsx # Edit item
│ │ │ └── items.new.tsx # Create item
│ │ └── api/
│ │ └── auth/
│ │ └── $.ts # better-auth API catch-all
│ ├── components/
│ │ ├── ui/ # shadcn/ui components (auto-installed)
│ │ ├── app-sidebar.tsx # Navigation sidebar
│ │ ├── theme-toggle.tsx # Light/dark/system toggle
│ │ ├── user-nav.tsx # User dropdown menu
│ │ └── stat-card.tsx # Dashboard stat card
│ ├── db/
│ │ ├── schema.ts # Drizzle schema (all tables)
│ │ └── index.ts # Drizzle client factory
│ ├── lib/
│ │ ├── auth.server.ts # better-auth server config
│ │ ├── auth.client.ts # better-auth React hooks
│ │ └── utils.ts # cn() helper for shadcn/ui
│ ├── server/
│ │ └── functions.ts # Server functions (CRUD, auth checks)
│ ├── styles/
│ │ └── app.css # Tailwind v4 + shadcn/ui CSS variables
│ ├── router.tsx # TanStack Router configuration
│ ├── client.tsx # Client entry (hydrateRoot)
│ ├── ssr.tsx # SSR entry
│ └── routeTree.gen.ts # Auto-generated route tree (do not edit)
├── drizzle/ # Generated migrations
├── public/ # Static assets (favicon, etc.)
├── vite.config.ts
├── wrangler.jsonc
├── drizzle.config.ts
├── tsconfig.json
├── package.json
├── .dev.vars # Local env vars (NOT committed)
└── .gitignore
依赖项
运行时:
{
"react": "^19.0.0",
"react-dom": "^19.0.0",
"@tanstack/react-router": "^1.120.0",
"@tanstack/react-start": "^1.120.0",
"drizzle-orm": "^0.38.0",
"better-auth": "^1.2.0",
"zod": "^3.24.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"tailwind-merge": "^3.0.0",
"lucide-react": "^0.480.0"
}
开发:
{
"@cloudflare/vite-plugin": "^1.0.0",
"@tailwindcss/vite": "^4.0.0",
"@vitejs/plugin-react": "^4.4.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.7.0",
"drizzle-kit": "^0.30.0",
"wrangler": "^4.0.0",
"tw-animate-css": "^1.2.0"
}
脚本:
{
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"deploy": "wrangler deploy",
"db:generate": "drizzle-kit generate",
"db:migrate:local": "wrangler d1 migrations apply PROJECT_NAME-db --local",
"db:migrate:remote": "wrangler d1 migrations apply PROJECT_NAME-db --remote"
}
工作流
步骤 1:收集项目信息
| 必填 | 可选 |
|---|---|
| 项目名称(kebab-case) | Google OAuth 凭据 |
| 一句话描述 | 自定义域名 |
| Cloudflare 账户 | 是否需要 R2 存储? |
| 认证方式:Google OAuth、邮箱/密码,或两者兼用 | 管理员邮箱 |
步骤 2:初始化项目
从头开始创建项目目录和所有配置文件。
vite.config.ts — 插件顺序很重要。Cloudflare 必须排在第一位:
import { defineConfig } from "vite";
import { cloudflare } from "@cloudflare/vite-plugin";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import tailwindcss from "@tailwindcss/vite";
import viteReact from "@vitejs/plugin-react";
export default defineConfig({
plugins: [
cloudflare({ viteEnvironment: { name: "ssr" } }),
tailwindcss(),
tanstackStart(),
viteReact(),
],
});
wrangler.jsonc:
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "PROJECT_NAME",
"compatibility_date": "2025-04-01",
"compatibility_flags": ["nodejs_compat"],
"main": "@tanstack/react-start/server-entry",
"account_id": "ACCOUNT_ID",
"d1_databases": [
{
"binding": "DB",
"database_name": "PROJECT_NAME-db",
"database_id": "DATABASE_ID",
"migrations_dir": "drizzle"
}
]
}
要点:main 必须是 "@tanstack/react-start/server-entry"(Nitro 服务端入口)。使用 nodejs_compat(而非 node_compat)。添加 account_id,以避免出现交互式提示。
tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"paths": { "@/*": ["./src/*"] },
"types": ["@cloudflare/workers-types/2023-07-01"]
},
"include": ["src/**/*", "vite.config.ts"]
}
.dev.vars — 使用 openssl rand -hex 32 生成 BETTER_AUTH_SECRET:
BETTER_AUTH_SECRET=<generated-hex-32>
BETTER_AUTH_URL=http://localhost:3000
TRUSTED_ORIGINS=http://localhost:3000
# GOOGLE_CLIENT_ID=
# GOOGLE_CLIENT_SECRET=
.gitignore — node_modules、.wrangler、dist、.output、.dev.vars、.vinxi、.DS_Store
然后安装依赖并创建 D1 数据库:
cd PROJECT_NAME && pnpm install
npx wrangler d1 create PROJECT_NAME-db
# Copy the database_id into wrangler.jsonc d1_databases binding
步骤 3:数据库架构
src/db/schema.ts — 所有表。better-auth 需要:users、sessions、accounts、verifications。添加应用表(例如 items)用于 CRUD 演示。
D1 特定规则:
- 使用
integer存储时间戳(Unix 纪元),不要使用 Date 对象 - 使用
text作为主键(nanoid/cuid2),不要使用自增 - 每个查询的绑定参数保持在 100 个以内(大量插入时使用批处理)
- D1 中始终启用外键
src/db/index.ts — Drizzle 客户端工厂:
import { drizzle } from "drizzle-orm/d1";
import { env } from "cloudflare:workers";
import * as schema from "./schema";
export function getDb() {
return drizzle(env.DB, { schema });
}
重要:使用 import { env } from "cloudflare:workers"——不要使用 process.env。在每个服务端函数内(每个请求一次)创建 Drizzle 客户端,不要在模块级别创建。
drizzle.config.ts:
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./drizzle",
dialect: "sqlite",
});
生成并应用初始迁移:
pnpm db:generate
pnpm db:migrate:local
步骤 4:配置认证
src/lib/auth.server.ts — 服务端 better-auth:
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { drizzle } from "drizzle-orm/d1";
import { env } from "cloudflare:workers";
import * as schema from "../db/schema";
export function getAuth() {
const db = drizzle(env.DB, { schema });
return betterAuth({
database: drizzleAdapter(db, { provider: "sqlite" }),
secret: env.BETTER_AUTH_SECRET,
baseURL: env.BETTER_AUTH_URL,
trustedOrigins: env.TRUSTED_ORIGINS?.split(",") ?? [],
emailAndPassword: { enabled: true },
socialProviders: {
// Add Google OAuth if credentials provided
},
});
}
重要:必须按请求调用 getAuth()(在处理程序/加载器内),不要在模块级别调用。
src/lib/auth.client.ts — 客户端认证钩子:
import { createAuthClient } from "better-auth/react";
export const { useSession, signIn, signOut, signUp } = createAuthClient();
src/routes/api/auth/$.ts — better-auth 的 API 全捕获路由:
import { createAPIFileRoute } from "@tanstack/react-start/api";
import { getAuth } from "../../../lib/auth.server";
export const APIRoute = createAPIFileRoute("/api/auth/$")({
GET: ({ request }) => getAuth().handler(request),
POST: ({ request }) => getAuth().handler(request),
});
重要:认证必须使用 API 路由(createAPIFileRoute),而不是服务端函数(createServerFn)。better-auth 需要直接访问请求/响应。
步骤 5:服务端函数
核心模式——始终在处理程序内创建 DB 客户端:
import { createServerFn } from "@tanstack/react-start";
import { getDb } from "../db";
export const getItems = createServerFn({ method: "GET" }).handler(async () => {
const db = getDb();
return db.select().from(items).all();
});
使用 Zod 验证输入:
export const createItem = createServerFn({ method: "POST" })
.inputValidator(
z.object({
name: z.string().min(1),
description: z.string().optional(),
})
)
.handler(async ({ data }) => {
const db = getDb();
const id = crypto.randomUUID();
await db.insert(items).values({ id, ...data, createdAt: Date.now() });
return { id };
});
受保护的服务端函数——检查认证状态,未认证时抛出重定向:
import { redirect } from "@tanstack/react-router";
import { getAuth } from "../lib/auth.server";
async function requireSession(request?: Request) {
const auth = getAuth();
const session = await auth.api.getSession({
headers: request?.headers ?? new Headers(),
});
if (!session) {
throw redirect({ to: "/login" });
}
return session;
}
export const getSessionFn = createServerFn({ method: "GET" }).handler(
async ({ request }) => {
const auth = getAuth();
return auth.api.getSession({ headers: request.headers });
}
);
export const getItems = createServerFn({ method: "GET" }).handler(
async ({ request }) => {
const session = await requireSession(request);
const db = getDb();
return db.select().from(items).where(eq(items.userId, session.user.id)).all();
}
);
路由加载器模式——在路由 loader 中调用服务端函数:
export const Route = createFileRoute("/_authed/items")({
loader: () => getItems(),
component: ItemsPage,
});
function ItemsPage() {
const items = Route.useLoaderData();
return <div>{items.map((item) => <div key={item.id}>{item.name}</div>)}</div>;
}
认证守卫(_authed.tsx)——使用 beforeLoad:
export const Route = createFileRoute("/_authed")({
beforeLoad: async () => {
const session = await getSessionFn();
if (!session) {
throw redirect({ to: "/login" });
}
return { session };
},
});
子路由通过 Route.useRouteContext() 访问会话。
变更操作 + 失效处理——变更后,使路由器失效,以重新获取加载器数据:
function CreateItemForm() {
const router = useRouter();
const handleSubmit = async (data: NewItem) => {
await createItem({ data });
router.invalidate();
router.navigate({ to: "/items" });
};
return <form onSubmit={...}>...</form>;
}
类型安全——使用 Drizzle 的 InferSelectModel / InferInsertModel 定义服务端函数的输入/输出类型。认证失败时,始终使用 throw redirect(),而不是返回错误响应。
步骤 6:应用外壳 + 主题
src/routes/__root.tsx — 完整的 HTML 文档,包含来自 @tanstack/react-router 的 <HeadContent /> + <Scripts />,在 <html> 上设置 suppressHydrationWarning(SSR + 主题),内联主题初始化脚本以防止闪烁,并导入全局 CSS。
src/styles/app.css — @import "tailwindcss"(v4 语法)+ :root 和 .dark 中的 shadcn/ui CSS 变量。仅使用语义化令牌。
src/router.tsx:
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";
export function createRouter() {
return createTanStackRouter({ routeTree });
}
declare module "@tanstack/react-router" {
interface Register {
router: ReturnType<typeof createRouter>;
}
}
src/client.tsx + src/ssr.tsx — 标准 TanStack Start 入口样板代码。
安装 shadcn/ui:
pnpm dlx shadcn@latest init --defaults
pnpm dlx shadcn@latest add button card input label sidebar table dropdown-menu form separator sheet
主题切换 — 三种状态(浅色 → 深色 → 系统 → 浅色),持久化到 localStorage,并在 <html> 上设置 .dark 类。仅通过 JS 检测系统偏好;不使用 CSS @media (prefers-color-scheme) 查询。
src/components/ 中的组件:app-sidebar.tsx、theme-toggle.tsx、user-nav.tsx、stat-card.tsx。
步骤 7:CRUD 服务端函数
| 函数 | 方法 | 用途 |
|---|---|---|
getItems | GET | 列出当前用户的所有项目 |
getItem | GET | 按 ID 获取单个项目 |
createItem | POST | 创建新项目 |
updateItem | POST | 更新现有项目 |
deleteItem | POST | 按 ID 删除项目 |
每个服务端函数:(1) 获取身份验证会话;(2) 通过 getDb() 创建每个请求独立的 Drizzle 客户端;(3) 执行数据库操作;(4) 返回类型化数据。路由加载器调用 GET 函数。变更操作调用 POST 函数,然后执行 router.invalidate()。
步骤 8:本地验证
pnpm dev
- 应用可在 http://localhost:3000 加载
- 注册新账户(邮箱/密码)
- 登录和退出功能正常
- 控制面板加载并显示统计卡片
- 创建、列出、编辑、删除项目
- 主题切换顺序为:浅色 -> 深色 -> 系统
- 侧边栏在移动设备上可折叠
- 控制台无错误
步骤 9:部署到生产环境
部署前检查清单 — 运行部署前请确认:
-
wrangler.jsonc中的account_id正确;main为"@tanstack/react-start/server-entry";compatibility_flags中包含nodejs_compat - 已创建 D1 数据库并设置
database_id -
.dev.vars已加入 gitignore;源代码中没有硬编码的密钥
设置生产环境密钥:
openssl rand -hex 32 | npx wrangler secret put BETTER_AUTH_SECRET
echo "https://PROJECT.SUBDOMAIN.workers.dev" | npx wrangler secret put BETTER_AUTH_URL
echo "http://localhost:3000,https://PROJECT.SUBDOMAIN.workers.dev" | npx wrangler secret put TRUSTED_ORIGINS
# Google OAuth (optional)
echo "your-client-id" | npx wrangler secret put GOOGLE_CLIENT_ID
echo "your-client-secret" | npx wrangler secret put GOOGLE_CLIENT_SECRET
如果使用 Google OAuth,请在 Google Cloud Console 中添加生产环境重定向 URI:https://PROJECT.SUBDOMAIN.workers.dev/api/auth/callback/google。
迁移并部署:
pnpm db:migrate:remote
pnpm build && npx wrangler deploy
首次部署后,将 BETTER_AUTH_URL 更新为实际的 Worker URL,然后重新部署。
**验证:**应用可在生产环境 URL 加载,身份验证正常,CRUD 功能正常,主题设置会保留。
自定义域名(可选):Cloudflare Dashboard → Workers → Triggers → Custom Domains。将 BETTER_AUTH_URL + TRUSTED_ORIGINS 密钥 + Google OAuth 重定向 URI 更新为新域名。重新部署。
常见问题
| 症状 | 原因 | 解决方法 |
|---|---|---|
env 未定义 | 在模块级别访问 | 仅在请求处理程序内部使用 import { env } from "cloudflare:workers" |
| 未找到 D1 数据库 | 绑定不匹配 | 检查 wrangler.jsonc 中的 d1_databases 绑定名称是否与代码一致 |
| 认证重定向循环 | URL 不匹配 | BETTER_AUTH_URL 必须与实际 URL 完全一致(协议 + 域名,末尾不带斜杠) |
| 认证静默失败 | 缺少来源 | 设置 TRUSTED_ORIGINS 密钥,并填入所有有效 URL(以逗号分隔) |
| 样式未加载 | 缺少插件 | 确保 vite.config.ts 中包含 @tailwindcss/vite 插件 |
| SSR 水合不匹配 | 主题闪烁 | 为 <html> 元素添加 suppressHydrationWarning |
| 在 Cloudflare 上构建失败 | 配置错误 | 检查 wrangler.jsonc 中的 nodejs_compat 标志和 main 字段 |
| 密钥未生效 | 未重新部署 | wrangler secret put 不会重新部署——之后运行 npx wrangler deploy |
| 认证端点返回 404 | 路由类型错误 | 使用 createAPIFileRoute(API 路由),而不是将 createServerFn 用于 better-auth |
| “redirect_uri_mismatch” | 缺少 URI | 将生产环境 URL 添加到 Google Cloud Console OAuth 重定向 URI 中 |
| Vite 报错晦涩难懂 | 插件顺序 | 顺序必须为:cloudflare() -> tailwindcss() -> tanstackStart() -> viteReact() |
| “Table not found” 500 错误 | 缺少迁移 | 部署前运行 pnpm db:migrate:remote |