tanstack-start について
Claude に Cloudflare Workers 向けの TanStack Start フルスタックアプリをゼロから構築させるスキルです。構成、認証、D1 データベース、CRUD、ローカル確認、デプロイを案内します。
主な機能
- 01プロジェクトファイルをゼロから生成
- 02TanStack Start の SSR とファイルベースルーティング
- 03better-auth の認証設定
- 04D1 と Drizzle のデータベースパターン
- 05CRUD サーバー関数とデプロイ手順
活用シーン
- 01Cloudflare Workers でフルスタックアプリを開始
- 02メールとパスワードまたは Google OAuth の認証を追加
- 03認証付き CRUD ワークフローを実装
- 04TanStack Start アプリを確認してデプロイ
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill tanstack-startインストール後、Claude Code で/tanstack-startを入力して読み込まれたか確認します。
詳細
Cloudflare での TanStack Start
ゼロから完全なフルスタックアプリを構築します。Claude がすべてのファイルを生成するため、テンプレートのクローンやスキャフォールドコマンドは不要です。
スタック:Cloudflare Workers 上の TanStack Start v1(SSR、ファイルベースルーティング、Nitro によるサーバー関数)、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:プロジェクト情報の収集
| 必須 | 任意 |
|---|---|
| プロジェクト名(ケバブケース) | Google OAuth の認証情報 |
| 1 行の説明 | カスタムドメイン |
| 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 が必要です。CRUD のデモ用に、アプリケーションのテーブル(例:items)を追加します。
D1 固有のルール:
- タイムスタンプには Unix エポックの
integerを使用し、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 });
}
重要:process.env ではなく、import { env } from "cloudflare:workers" を使用してください。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),
});
重要:認証にはサーバー関数(createServerFn)ではなく、API ルート(createAPIFileRoute)を必ず使用してください。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 — @tanstack/react-router の <HeadContent /> と <Scripts /> を含む完全な HTML ドキュメントです。SSR とテーマのため、<html> に suppressHydrationWarning を設定し、画面のちらつきを防ぐインラインのテーマ初期化スクリプトと、グローバル 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
テーマ切り替え — 3 つの状態(ライト → ダーク → システム → ライト)を切り替え、localStorage に保存し、<html> に .dark クラスを設定します。システム設定の検出は JS のみで行います。@media (prefers-color-scheme) の CSS クエリは使用しません。
コンポーネント(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 と完全に一致している必要があります(プロトコル+ドメイン、末尾のスラッシュなし) |
| 認証が何も表示せずに失敗する | オリジンの不足 | 有効なすべての URL を含む TRUSTED_ORIGINS シークレットを設定します(カンマ区切り) |
| スタイルが読み込まれない | プラグインの不足 | vite.config.ts に @tailwindcss/vite プラグインがあることを確認します |
| SSR のハイドレーション不一致 | テーマのちらつき | <html> 要素に suppressHydrationWarning を追加します |
| Cloudflare でビルドに失敗する | 設定の誤り | wrangler.jsonc の nodejs_compat フラグと main フィールドを確認します |
| シークレットが反映されない | 再デプロイされていない | wrangler secret put では再デプロイされません。実行後に npx wrangler deploy を実行します |
| 認証エンドポイントが 404 を返す | ルートタイプの誤り | better-auth には createServerFn ではなく、API ルートの createAPIFileRoute を使用します |
| "redirect_uri_mismatch" | URI の不足 | Google Cloud Console の OAuth リダイレクト URI に本番 URL を追加します |
| Vite の不可解なエラー | プラグインの順序 | 順序は cloudflare() -> tailwindcss() -> tanstackStart() -> viteReact() にする必要があります |
| "Table not found" の 500 エラー | マイグレーションの不足 | デプロイ前に pnpm db:migrate:remote を実行します |