mcpField
T
tanstack-start

tanstack-start

github.com/jezweb开发1.1k2.5k 次安装3 周前更新
代码仓库

关于 tanstack-start

指导 Claude 从零创建部署于 Cloudflare Workers 的 TanStack Start 全栈应用,涵盖项目结构、身份验证、D1 数据库访问、CRUD 流程、本地检查与部署。

主要功能

  1. 01从零生成项目文件
  2. 02TanStack Start SSR 与文件路由
  3. 03better-auth 身份验证配置
  4. 04D1 与 Drizzle 数据库模式
  5. 05CRUD 服务端函数与部署步骤

适用场景

  1. 01在 Cloudflare Workers 上启动全栈应用
  2. 02添加邮箱密码或 Google OAuth 身份验证
  3. 03实现经过身份验证的 CRUD 工作流
  4. 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 服务端函数
函数方法用途
getItemsGET列出当前用户的所有项目
getItemGET按 ID 获取单个项目
createItemPOST创建新项目
updateItemPOST更新现有项目
deleteItemPOST按 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

评论

支持 Markdown