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. 01TanStack Startアプリのルート構築
  2. 02データベースや環境変数への安全なアクセス
  3. 03サーバー関数への認証ミドルウェア追加
  4. 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 を使用するのは、緊急のホットフィックスに限ります。

ルール

  1. ローダー内で process.env に直接アクセスしないでください — createServerFn または createServerOnlyFn を使用します
  2. ローダーはアイソモーフィックです — ナビゲーション中はサーバーとクライアントの両方で実行されます
  3. サーバー関数が境界となります — DB、環境変数、シークレットに触れる処理はすべて createServerFn を経由させます
  4. 内部データアクセスには、API ルートよりもサーバー関数を優先してください — 型安全で、手動の fetch は不要です
  5. コンポーネントからサーバー関数を呼び出すときは、直接呼び出すのではなく useServerFn() フックを使用します
  6. ミドルウェアは合成できます — 認証、検証、ロギングを再利用可能なミドルウェアとして積み重ねます

更新され続けるドキュメント

gremlin-cms の構築に伴い、このスキルを拡充します。開発中に見つかったパターンを追加してください。

コメント