tanstack-start について
TanStack Start は、型安全な Web アプリを構築するためのフルスタック React フレームワークです。サーバー関数、SSR、ストリーミング、API ルートを TanStack Router と Vite/Nitro の構成で扱えます。
主な機能
- 01型安全なサーバー関数
- 02サーバーサイドレンダリング
- 03遅延データのストリーミング
- 04ファイルベースルーティング
- 05HTTP API ルート
活用シーン
- 01フルスタック React アプリの構築
- 02データをストリーミングするページの配信
- 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 の機能を構築するワークフロー
- アプリを設定する — Vite プラグインを使用して
app.config.tsを設定し、デプロイ先に合ったserver.presetを指定します。 - ルートを定義する — HTML ドキュメントのシェル(
<html>、<head>、<body>、<Outlet>、<ScrollRestoration>、<Scripts>)を含むsrc/routes/__root.tsxを作成します。 - サーバー関数を記述する — サーバー上で実行する必要のあるロジック(データベースアクセス、シークレット、ファイル I/O)には、
.validator()と.handler()を使ってcreateServerFn()を使用します。 - サーバー関数をルートに組み込む — 読み取りにはルートの
loaderからサーバー関数を呼び出し、書き込みにはミューテーションフック(useMutation)を使用します。 - 生の HTTP 用に API ルートを追加する — Webhook、OAuth コールバック、外部サービスから直接呼び出されるエンドポイントには
createAPIFileRouteを使用します。 - 重要度の低いデータをストリーミングする — 遅くてもブロックしないローダーデータを
defer()でラップし、<Suspense>と<Await>でレンダリングします。 - デプロイする — 対象プラットフォームに適した
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の読み取りを混在させること。
コメント
Markdown 対応