mcpField
T
tanstack-start

tanstack-start

github.com/mindrally開発28869 回インストール3 週間前に更新
リポジトリ

tanstack-start について

TanStack Start は、型安全な Web アプリを構築するためのフルスタック React フレームワークです。サーバー関数、SSR、ストリーミング、API ルートを TanStack Router と Vite/Nitro の構成で扱えます。

主な機能

  1. 01型安全なサーバー関数
  2. 02サーバーサイドレンダリング
  3. 03遅延データのストリーミング
  4. 04ファイルベースルーティング
  5. 05HTTP API ルート

活用シーン

  1. 01フルスタック React アプリの構築
  2. 02データをストリーミングするページの配信
  3. 03Webhook と OAuth コールバックの実装

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mindrally/skills --skill tanstack-start

インストール後、Claude Code で/tanstack-startを入力して読み込まれたか確認します。

詳細

TanStack Start

TanStack Start は、TanStack Router と Vite/Nitro のビルドパイプライン上にサーバー関数、SSR、ストリーミング、API ルートを組み合わせ、サーバーからクライアントまで一貫した型安全性を実現するフルスタック React フレームワークです。

TanStack Start の機能を構築するワークフロー

  1. アプリを設定する — Vite プラグインを使用して app.config.ts を設定し、デプロイ先に合った server.preset を指定します。
  2. ルートを定義する — HTML ドキュメントのシェル(<html>、<head>、<body>、<Outlet>、<ScrollRestoration>、<Scripts>)を含む src/routes/__root.tsx を作成します。
  3. サーバー関数を記述する — サーバー上で実行する必要のあるロジック(データベースアクセス、シークレット、ファイル I/O)には、.validator() と .handler() を使って createServerFn() を使用します。
  4. サーバー関数をルートに組み込む — 読み取りにはルートの loader からサーバー関数を呼び出し、書き込みにはミューテーションフック(useMutation)を使用します。
  5. 生の HTTP 用に API ルートを追加する — Webhook、OAuth コールバック、外部サービスから直接呼び出されるエンドポイントには createAPIFileRoute を使用します。
  6. 重要度の低いデータをストリーミングする — 遅くてもブロックしないローダーデータを defer() でラップし、<Suspense> と <Await> でレンダリングします。
  7. デプロイする — 対象プラットフォームに適した server.preset を選び、リリース前に環境変数へのアクセス規則を確認します。

コア原則

  • TanStack Start = TanStack Router + Vite + Nitro。これにより、型安全な単一のコードベースでフルスタック React を実現します。
  • createServerFn はサーバーサイドロジックの主要な仕組みです。内部データへのアクセスには、独自に作成した REST エンドポイントよりも優先して使用してください。
  • TanStack Router の規約はすべて Start にも適用されます。ファイルベースのルーティング、ローダー、検索パラメーターの検証、ルーターコンテキストなどが含まれます。
  • ストリーミングと Suspense は中核機能です。初期レスポンスをブロックすべきでないものには defer() を使用してください。
  • サーバー専用コード(シークレット、DB クライアント、ファイルシステムへのアクセス)は、必ずサーバー関数または API ルート内に置いてください。クライアントコンポーネントにインポートしてはいけません。

設定

app.config.ts
import { defineConfig } from '@tanstack/start/config'
import tsConfigPaths from 'vite-tsconfig-paths'

export default defineConfig({
  vite: { plugins: [tsConfigPaths()] },
  server: {
    preset: 'node-server', // or: 'vercel', 'netlify', 'bun', 'cloudflare-pages'
  },
})
ルートの HTML シェル
// src/routes/__root.tsx
import { createRootRoute, Outlet, ScrollRestoration } from '@tanstack/react-router'
import { Scripts } from '@tanstack/start'

export const Route = createRootRoute({
  component: () => (
    <html lang="en">
      <head />
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  ),
})

サーバー関数

  • 見つけやすいように、専用の src/server/functions/ モジュール(または同じ場所に配置した *.server.ts)でサーバー関数を定義します。
  • ハンドラーの実行前に、必ず .validator() で入力を検証してください(一般的には Zod を使用します)。クライアントから渡された data を決して信用してはいけません。
  • ハンドラーからは、内容が明確な Error(または型付きエラー)をスローします。TanStack Start はスローされたエラーをクライアントにシリアライズして返します。
  • サーバー関数はローダー、アクション、イベントハンドラー、useMutation から直接呼び出せます。クライアント上では通常の非同期関数のように動作しますが、実行されるのはサーバー上です。
// src/server/functions/posts.ts
import { createServerFn } from '@tanstack/start'
import { z } from 'zod'
import { db } from '~/server/db'

export const getPost = createServerFn()
  .validator(z.object({ id: z.string() }))
  .handler(async ({ data }) => {
    const post = await db.post.findUnique({ where: { id: data.id } })
    if (!post) throw new Error('Post not found')
    return post
  })

export const createPost = createServerFn()
  .validator(z.object({ title: z.string().min(1), body: z.string() }))
  .handler(async ({ data }) => db.post.create({ data }))
ルートでサーバー関数を使用する
export const Route = createFileRoute('/posts/$postId')({
  loader: ({ params }) => getPost({ data: { id: params.postId } }),
  component: PostDetail,
})

function PostDetail() {
  const post = Route.useLoaderData()
  return <h1>{post.title}</h1>
}
サーバー関数によるミューテーション
import { useMutation } from '@tanstack/react-query'
import { useQueryClient } from '@tanstack/react-query'
import { createPost } from '~/server/functions/posts'

function NewPostForm() {
  const queryClient = useQueryClient()
  const mutation = useMutation({
    mutationFn: (input: { title: string; body: string }) => createPost({ data: input }),
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }),
  })

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        const form = new FormData(e.currentTarget)
        mutation.mutate({
          title: String(form.get('title')),
          body: String(form.get('body')),
        })
      }}
    >
      <input name="title" placeholder="Title" required />
      <textarea name="body" placeholder="Body" required />
      <button type="submit" disabled={mutation.isPending}>Publish</button>
    </form>
  )
}

API ルート

Webhook、OAuth コールバック、ヘルスチェックなど、生の HTTP を扱う必要があるエンドポイントには、API ルートを使用してください。API ルートとは異なり、サーバー関数は自分のクライアントコードから呼び出すことを想定しています。

// src/routes/api/webhook.ts
import { createAPIFileRoute } from '@tanstack/start/api'

export const Route = createAPIFileRoute('/api/webhook')({
  POST: async ({ request }) => {
    const body = await request.json()
    // verify signature, enqueue processing, etc.
    return Response.json({ received: true })
  },
})

defer() によるストリーミング

  • 初期描画に必要なデータだけを await し、それ以外はすべて defer() でラップして、シェルをすぐにレンダリングできるようにします。
  • 遅延させた値を <Suspense> と <Await> と組み合わせ、プロミスが解決するまで UI にフォールバックを表示します。
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    const post = await getPost({ data: { id: params.postId } }) // critical, awaited
    const comments = getComments({ data: { postId: params.postId } }) // not awaited
    return { post, comments: defer(comments) }
  },
  component: PostDetail,
})

function PostDetail() {
  const { post, comments } = Route.useLoaderData()
  return (
    <div>
      <h1>{post.title}</h1>
      <Suspense fallback={<CommentsSkeleton />}>
        <Await promise={comments}>{(c) => <CommentsList comments={c} />}</Await>
      </Suspense>
    </div>
  )
}

環境変数

  • サーバー専用のシークレットにアクセスする場合は、サーバー関数または API ルート内でのみ process.env を使用します。
  • クライアントバンドルに公開する必要のある変数には import.meta.env.VITE_* を使用します。
  • クライアントコンポーネント内で process.env を直接参照してはいけません。Vite はサーバーのシークレットをインライン化しません。また、これを行うと設定情報が漏洩する一般的な原因になります。

デプロイ先

ホスティングプラットフォームに合わせて、app.config.ts の server.preset を設定します。

  • node-server — デフォルト。Node.js が動作する環境ならどこでも実行できます。
  • vercel — Vercel のサーバーレス関数/エッジ関数。
  • netlify — Netlify Functions。
  • bun — Bun ランタイム。
  • cloudflare-pages — Cloudflare Pages + Workers(サーバー関数で Node API の互換性に不足がないか確認してください)。

よくある間違い

  • createServerFn を経由せず、クライアントコンポーネントからサーバー関数のハンドラーロジックを直接呼び出すこと。これによりクライアントとサーバーの境界が崩れ、型推論が機能しなくなります。
  • ローダー内のすべての処理を await すること。ストリーミングの目的が損なわれ、初期レスポンスが遅くなります。
  • .validator() を忘れること。ハンドラーが、検証されていないクライアント入力を信頼することになります。
  • クライアントコードとサーバーコードの両方からインポートされる共有ユーティリティモジュールで、process.env の読み取りを混在させること。

コメント