jpskill.com
🛠️ 開発・MCP コミュニティ

tanstack-start

TanStack Start を使って、型安全なサーバー機能やファイルベースルーティングを備えたフルスタックReactアプリを構築し、API作成やサーバーサイドデータ取得、クラウド環境へのデプロイを支援するSkill。

📜 元の英語説明(参考)

Build full-stack React apps with TanStack Start. Use when a user asks to create a full-stack React application with type-safe server functions, set up file-based routing with SSR/SSG/SPA modes, build APIs with middleware and validation, implement server-side data fetching with TanStack Router, or deploy to Cloudflare/Netlify/Vercel/Node.

🇯🇵 日本人クリエイター向け解説

一言でいうと

TanStack Start を使って、型安全なサーバー機能やファイルベースルーティングを備えたフルスタックReactアプリを構築し、API作成やサーバーサイドデータ取得、クラウド環境へのデプロイを支援するSkill。

※ jpskill.com 編集部が日本のビジネス現場向けに補足した解説です。Skill本体の挙動とは独立した参考情報です。

⚡ おすすめ: コマンド1行でインストール(60秒)

下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。

🍎 Mac / 🐧 Linux
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o tanstack-start.zip https://jpskill.com/download/15454.zip && unzip -o tanstack-start.zip && rm tanstack-start.zip
🪟 Windows (PowerShell)
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/15454.zip -OutFile "$d\tanstack-start.zip"; Expand-Archive "$d\tanstack-start.zip" -DestinationPath $d -Force; ri "$d\tanstack-start.zip"

完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。

💾 手動でダウンロードしたい(コマンドが難しい人向け)
  1. 1. 下の青いボタンを押して tanstack-start.zip をダウンロード
  2. 2. ZIPファイルをダブルクリックで解凍 → tanstack-start フォルダができる
  3. 3. そのフォルダを C:\Users\あなたの名前\.claude\skills\(Win)または ~/.claude/skills/(Mac)へ移動
  4. 4. Claude Code を再起動

⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。

🎯 このSkillでできること

下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。

📦 インストール方法 (3ステップ)

  1. 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
  2. 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
  3. 3. 展開してできたフォルダを、ホームフォルダの .claude/skills/ に置く
    • · macOS / Linux: ~/.claude/skills/
    • · Windows: %USERPROFILE%\.claude\skills\

Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。

詳しい使い方ガイドを見る →
最終更新
2026-05-18
取得日時
2026-05-18
同梱ファイル
1

📖 Skill本文(日本語訳)

※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。

TanStack Start

概要

TanStack Start は、TanStack Router と Vite を基盤とするフルスタック React フレームワークです。型安全なサーバー関数 (createServerFn)、ローダーによるファイルベースルーティング、構成可能なミドルウェア、ネットワーク境界を越えた Zod バリデーション、柔軟なレンダリングモード (SSR, SSG, SPA, ISR) を提供します。Next.js とは異なり、Vite (Webpack ではない) を使用し、あらゆるデプロイターゲットをサポートし、サーバー/クライアントのコード境界を明示的に制御できます。

手順

ステップ 1: プロジェクトのセットアップ

npx create-start@latest my-app
cd my-app
npm install
npm run dev
my-app/
├── app/
│   ├── routes/
│   │   ├── __root.tsx          # ルートレイアウト
│   │   ├── index.tsx           # / ルート
│   │   ├── about.tsx           # /about ルート
│   │   ├── posts/
│   │   │   ├── index.tsx       # /posts ルート
│   │   │   └── $postId.tsx     # /posts/:postId 動的ルート
│   │   └── api/
│   │       └── health.ts       # /api/health サーバールート
│   ├── utils/
│   │   ├── posts.functions.ts  # サーバー関数ラッパー
│   │   ├── posts.server.ts     # サーバー専用 DB クエリ
│   │   └── schemas.ts          # 共有 Zod スキーマ
│   ├── router.tsx
│   └── client.tsx
├── app.config.ts               # TanStack Start 設定
└── package.json

ステップ 2: サーバー関数

サーバー関数はコアとなるプリミティブです。サーバー専用のロジックを定義し、ローダー、コンポーネント、イベントハンドラーなど、どこからでも呼び出すことができます。ネットワーク境界を越えて、完全な型安全性が保たれます。

// app/utils/posts.server.ts — サーバー専用データベースクエリ
import { db } from '~/db'

export async function findPosts(limit: number) {
  return db.query.posts.findMany({
    limit,
    orderBy: (posts, { desc }) => [desc(posts.createdAt)],
    with: { author: true },
  })
}

export async function findPostById(id: string) {
  return db.query.posts.findFirst({
    where: (posts, { eq }) => eq(posts.id, id),
    with: { author: true, comments: { with: { author: true } } },
  })
}

export async function createPost(data: { title: string; content: string; authorId: string }) {
  return db.insert(posts).values(data).returning()
}
// app/utils/posts.functions.ts — バリデーション付きサーバー関数
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
import { findPosts, findPostById, createPost } from './posts.server'

// GET — 投稿の取得 (ローダーとコンポーネントから呼び出し可能)
export const getPosts = createServerFn({ method: 'GET' })
  .inputValidator(z.object({ limit: z.number().min(1).max(100).default(20) }))
  .handler(async ({ data }) => {
    return findPosts(data.limit)
  })

// GET — 単一の投稿の取得
export const getPost = createServerFn({ method: 'GET' })
  .inputValidator(z.object({ id: z.string().uuid() }))
  .handler(async ({ data }) => {
    const post = await findPostById(data.id)
    if (!post) throw notFound()
    return post
  })

// POST — 投稿の作成 (ミドルウェアによる認証が必要)
export const createNewPost = createServerFn({ method: 'POST' })
  .middleware([authMiddleware])
  .inputValidator(z.object({
    title: z.string().min(1).max(200),
    content: z.string().min(1).max(50000),
  }))
  .handler(async ({ data, context }) => {
    // context.user は authMiddleware から渡される
    return createPost({ ...data, authorId: context.user.id })
  })

ステップ 3: ローダーによるルーティング

// app/routes/posts/index.tsx — ローダー付きの投稿リストページ
import { createFileRoute } from '@tanstack/react-router'
import { getPosts } from '~/utils/posts.functions'

export const Route = createFileRoute('/posts/')({
  // ローダーは SSR 中にサーバーで実行され、レンダリング前にデータを取得します
  loader: () => getPosts({ data: { limit: 20 } }),

  component: PostsPage,
})

function PostsPage() {
  const posts = Route.useLoaderData()

  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            <Link to="/posts/$postId" params={{ postId: post.id }}>
              {post.title}
            </Link>
            <span> by {post.author.name}</span>
          </li>
        ))}
      </ul>
    </div>
  )
}
// app/routes/posts/$postId.tsx — 動的な投稿ページ
import { createFileRoute, notFound } from '@tanstack/react-router'
import { getPost } from '~/utils/posts.functions'

export const Route = createFileRoute('/posts/$postId')({
  loader: ({ params }) => getPost({ data: { id: params.postId } }),

  // Not-found のためのエラー境界
  notFoundComponent: () => <div>Post not found</div>,

  component: PostPage,
})

function PostPage() {
  const post = Route.useLoaderData()

  return (
    <article>
      <h1>{post.title}</h1>
      <p>By {post.author.name} · {new Date(post.createdAt).toLocaleDateString()}</p>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />

      <h2>Comments ({post.comments.length})</h2>
      {post.comments.map(comment => (
        <div key={comment.id}>
          <strong>{comment.author.name}</strong>
          <p>{comment.body}</p>
        </div>
      ))}
    </article>
  )
}

ステップ 4: ミドルウェア

認証、ロギング、レート制限のための構成可能なミドルウェア。チェーンは next() で順番に実行されます。


// app/middleware/auth.ts — 認証ミドルウェア
import { createMiddleware } from '@tanstack/react-start'
import { redirect } from '@tanstack/react-router'
import { getRequestHeader } from '@tanstack/react-start/server'
import { verifyToken } from '~/utils/auth.server'

// リクエストミドルウェア — これを使用するすべてのサーバーリクエストで実行されます
export const authMiddleware = createMiddleware()
  .server(async ({ next }) => {
    const token = getRequestHeader('Authorization')?.replace('Bearer ', '')

    if (!token) {
      throw redirect({ to: '/login' })
    }

    const user = await verifyToken(token)
    if (!user) {
      throw redirect({ to: '/login' })
    }

    // コンテキストを介して、次のミドルウェア/サーバー関数にユーザーを渡します
    return next({ context: { user } })
  })

// ロギング

(原文はここで切り詰められています)
📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開

TanStack Start

Overview

TanStack Start is a full-stack React framework built on TanStack Router and Vite. It provides type-safe server functions (createServerFn), file-based routing with loaders, composable middleware, Zod validation across the network boundary, and flexible rendering modes (SSR, SSG, SPA, ISR). Unlike Next.js, it uses Vite (not Webpack), supports any deployment target, and gives you explicit control over server/client code boundaries.

Instructions

Step 1: Project Setup

npx create-start@latest my-app
cd my-app
npm install
npm run dev
my-app/
├── app/
│   ├── routes/
│   │   ├── __root.tsx          # Root layout
│   │   ├── index.tsx           # / route
│   │   ├── about.tsx           # /about route
│   │   ├── posts/
│   │   │   ├── index.tsx       # /posts route
│   │   │   └── $postId.tsx     # /posts/:postId dynamic route
│   │   └── api/
│   │       └── health.ts       # /api/health server route
│   ├── utils/
│   │   ├── posts.functions.ts  # Server function wrappers
│   │   ├── posts.server.ts     # Server-only DB queries
│   │   └── schemas.ts          # Shared Zod schemas
│   ├── router.tsx
│   └── client.tsx
├── app.config.ts               # TanStack Start config
└── package.json

Step 2: Server Functions

Server functions are the core primitive — define server-only logic callable from anywhere (loaders, components, event handlers). They cross the network boundary with full type safety.

// app/utils/posts.server.ts — Server-only database queries
import { db } from '~/db'

export async function findPosts(limit: number) {
  return db.query.posts.findMany({
    limit,
    orderBy: (posts, { desc }) => [desc(posts.createdAt)],
    with: { author: true },
  })
}

export async function findPostById(id: string) {
  return db.query.posts.findFirst({
    where: (posts, { eq }) => eq(posts.id, id),
    with: { author: true, comments: { with: { author: true } } },
  })
}

export async function createPost(data: { title: string; content: string; authorId: string }) {
  return db.insert(posts).values(data).returning()
}
// app/utils/posts.functions.ts — Server functions with validation
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
import { findPosts, findPostById, createPost } from './posts.server'

// GET — fetch posts (callable from loaders and components)
export const getPosts = createServerFn({ method: 'GET' })
  .inputValidator(z.object({ limit: z.number().min(1).max(100).default(20) }))
  .handler(async ({ data }) => {
    return findPosts(data.limit)
  })

// GET — fetch single post
export const getPost = createServerFn({ method: 'GET' })
  .inputValidator(z.object({ id: z.string().uuid() }))
  .handler(async ({ data }) => {
    const post = await findPostById(data.id)
    if (!post) throw notFound()
    return post
  })

// POST — create post (requires auth via middleware)
export const createNewPost = createServerFn({ method: 'POST' })
  .middleware([authMiddleware])
  .inputValidator(z.object({
    title: z.string().min(1).max(200),
    content: z.string().min(1).max(50000),
  }))
  .handler(async ({ data, context }) => {
    // context.user comes from authMiddleware
    return createPost({ ...data, authorId: context.user.id })
  })

Step 3: Routes with Loaders

// app/routes/posts/index.tsx — Posts list page with loader
import { createFileRoute } from '@tanstack/react-router'
import { getPosts } from '~/utils/posts.functions'

export const Route = createFileRoute('/posts/')({
  // Loader runs on the server during SSR, fetches data before render
  loader: () => getPosts({ data: { limit: 20 } }),

  component: PostsPage,
})

function PostsPage() {
  const posts = Route.useLoaderData()

  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            <Link to="/posts/$postId" params={{ postId: post.id }}>
              {post.title}
            </Link>
            <span> by {post.author.name}</span>
          </li>
        ))}
      </ul>
    </div>
  )
}
// app/routes/posts/$postId.tsx — Dynamic post page
import { createFileRoute, notFound } from '@tanstack/react-router'
import { getPost } from '~/utils/posts.functions'

export const Route = createFileRoute('/posts/$postId')({
  loader: ({ params }) => getPost({ data: { id: params.postId } }),

  // Error boundary for not-found
  notFoundComponent: () => <div>Post not found</div>,

  component: PostPage,
})

function PostPage() {
  const post = Route.useLoaderData()

  return (
    <article>
      <h1>{post.title}</h1>
      <p>By {post.author.name} · {new Date(post.createdAt).toLocaleDateString()}</p>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />

      <h2>Comments ({post.comments.length})</h2>
      {post.comments.map(comment => (
        <div key={comment.id}>
          <strong>{comment.author.name}</strong>
          <p>{comment.body}</p>
        </div>
      ))}
    </article>
  )
}

Step 4: Middleware

Composable middleware for auth, logging, rate limiting — chains execute in order with next().

// app/middleware/auth.ts — Authentication middleware
import { createMiddleware } from '@tanstack/react-start'
import { redirect } from '@tanstack/react-router'
import { getRequestHeader } from '@tanstack/react-start/server'
import { verifyToken } from '~/utils/auth.server'

// Request middleware — runs on all server requests that use it
export const authMiddleware = createMiddleware()
  .server(async ({ next }) => {
    const token = getRequestHeader('Authorization')?.replace('Bearer ', '')

    if (!token) {
      throw redirect({ to: '/login' })
    }

    const user = await verifyToken(token)
    if (!user) {
      throw redirect({ to: '/login' })
    }

    // Pass user to the next middleware / server function via context
    return next({ context: { user } })
  })

// Logging middleware — logs request timing
export const loggingMiddleware = createMiddleware()
  .server(async ({ next }) => {
    const start = Date.now()
    const result = await next()
    console.log(`Request took ${Date.now() - start}ms`)
    return result
  })

// Compose middleware — auth depends on logging
export const protectedMiddleware = createMiddleware()
  .middleware([loggingMiddleware, authMiddleware])
  .server(async ({ next, context }) => {
    // context.user is available from authMiddleware
    console.log(`Authenticated request from ${context.user.email}`)
    return next()
  })

Step 5: Server Functions in Components

// app/routes/posts/new.tsx — Form with server function mutation
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useServerFn } from '@tanstack/react-start'
import { createNewPost } from '~/utils/posts.functions'

export const Route = createFileRoute('/posts/new')({
  component: NewPostForm,
})

function NewPostForm() {
  const navigate = useNavigate()
  const createPost = useServerFn(createNewPost)

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    const formData = new FormData(e.currentTarget)

    const post = await createPost({
      data: {
        title: formData.get('title') as string,
        content: formData.get('content') as string,
      },
    })

    // Navigate to the new post
    navigate({ to: '/posts/$postId', params: { postId: post.id } })
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="title" placeholder="Post title" required />
      <textarea name="content" placeholder="Write your post..." required rows={10} />
      <button type="submit">Publish</button>
    </form>
  )
}

Step 6: Server Routes (API Endpoints)

// app/routes/api/health.ts — API-only route (no React component)
import { createAPIFileRoute } from '@tanstack/react-start/api'

export const APIRoute = createAPIFileRoute('/api/health')({
  GET: async ({ request }) => {
    return Response.json({
      status: 'ok',
      timestamp: new Date().toISOString(),
      version: process.env.APP_VERSION,
    })
  },
})
// app/routes/api/webhooks/stripe.ts — Webhook handler
import { createAPIFileRoute } from '@tanstack/react-start/api'

export const APIRoute = createAPIFileRoute('/api/webhooks/stripe')({
  POST: async ({ request }) => {
    const signature = request.headers.get('stripe-signature')
    const body = await request.text()

    const event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET)

    switch (event.type) {
      case 'checkout.session.completed':
        await handleCheckoutComplete(event.data.object)
        break
      case 'customer.subscription.updated':
        await handleSubscriptionUpdate(event.data.object)
        break
    }

    return Response.json({ received: true })
  },
})

Step 7: Rendering Modes

// app.config.ts — Configure rendering mode
import { defineConfig } from '@tanstack/react-start/config'
import vite from 'vite'

export default defineConfig({
  vite: {
    // Vite config
  },
  // Deploy targets
  server: {
    preset: 'node-server',        // or 'cloudflare-pages', 'netlify', 'vercel'
  },
})
// Static prerendering — generate at build time
// app/routes/about.tsx
export const Route = createFileRoute('/about')({
  // This page will be statically generated at build time
  staticData: { prerender: true },
  loader: () => getAboutContent(),
  component: AboutPage,
})

// Selective SSR — skip SSR for client-heavy pages
// app/routes/dashboard.tsx
export const Route = createFileRoute('/dashboard')({
  // This page will be a client-side SPA (no SSR)
  ssr: false,
  component: DashboardPage,
})

Guidelines

  • Server functions are the network boundarycreateServerFn replaces API routes for most use cases. Server code stays on the server, client gets type-safe RPC stubs.
  • File organization: .functions.ts for server function wrappers, .server.ts for server-only helpers, .ts for shared schemas. Static imports of .functions.ts are safe anywhere.
  • Loaders run before render — data is available immediately in components via Route.useLoaderData(). No loading spinners for initial page data.
  • Use useServerFn() hook in components to call server functions with proper error handling and loading states.
  • Middleware composes — build auth, logging, rate limiting as independent middleware and chain them. Context flows through the chain via next({ context }).
  • Zod validation in inputValidator — validates on both client and server, single schema definition, full TypeScript inference.
  • Deploy anywhere — Vite-based build outputs for Node.js, Cloudflare Workers/Pages, Netlify, Vercel, Deno. Change the server.preset in config.
  • TanStack Router underneath — all router features work (type-safe links, search params, nested layouts, error boundaries, pending states).
  • Use React Query for mutations — combine useServerFn with useMutation for optimistic updates, error handling, and cache invalidation.