tanstack-start について
tanstack-startは、TanStack Startプロジェクト向けにルーティング、サーバー関数、デプロイ手順を扱う開発スキルです。ローダーがサーバーとクライアントの両方で動く点を示し、型安全な境界で機密処理を分離します。
主な機能
- 01ファイルベースのルーティングと型付き定義
- 02型安全なサーバー関数とミドルウェア
- 03同形コードとサーバー専用コードの区別
- 04APIルートとTanStack Queryの利用
- 05デプロイ設定と問題の調査
活用シーン
- 01TanStack Startアプリのルート構築
- 02データベースや環境変数への安全なアクセス
- 03サーバー関数への認証ミドルウェア追加
- 04Vercelデプロイ後の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
})
サーバー関数
内部データアクセスにおいて REST/tRPC/GraphQL の代わりとなる、型安全な RPC です。ビルドプロセスによって、サーバー実装はクライアントバンドル内の 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()フック経由 - 他のサーバー関数 — サーバーロジックの合成
- イベントハンドラー — フォーム送信、クリック
サーバー専用関数
クライアントバンドルに絶対に含めてはならないユーティリティには、次を使用します。
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
モノレポ(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を使用します - ローダーはアイソモーフィックです — ナビゲーション中はサーバーとクライアントの両方で実行されます
- サーバー関数が境界となります — DB、環境変数、シークレットに触れる処理はすべて
createServerFnを経由させます - 内部データアクセスには、API ルートよりもサーバー関数を優先してください — 型安全で、手動の fetch は不要です
- コンポーネントからサーバー関数を呼び出すときは、直接呼び出すのではなく
useServerFn()フックを使用します - ミドルウェアは合成できます — 認証、検証、ロギングを再利用可能なミドルウェアとして積み重ねます
更新され続けるドキュメント
gremlin-cms の構築に伴い、このスキルを拡充します。開発中に見つかったパターンを追加してください。