- React
TanStack Start 1.0とは|使い方・サーバー関数・Next.js対応表
結論
TanStack Start は、TanStack Router を土台にした React のフルスタック・フレームワークです。2026年10月13日に「1.0 は安定版」と公式に発表されました。ルートごとに SSR のしかたを選べる「選択的 SSR」、型が通るサーバー関数(createServerFn)、クライアントとサーバーにまたがるミドルウェアが中心の機能で、React Server Components は実験的な機能として使えます。
この記事でわかること
- npm で新規作成して 127.0.0.1 で動かした出力と、サーバー関数の通信の中身
- サーバー関数の型エラー5例・CSRF の 403・import protection のエラー原文
- Next.js(App Router)との書き方の違いの対応表と、選ぶときの判断表
この記事で実際に動かしたもの:@tanstack/cli 0.72.16 で作った新規プロジェクト(@tanstack/react-start 1.169.4)を 127.0.0.1 の開発サーバーと本番ビルドで起動し、サーバー関数・ミドルウェア・選択的 SSR・Server Components(実験的)・CSRF ミドルウェアを curl と Chromium で確かめました。
TanStack Start のページは、最初の表示(サーバー)とその後の操作(ブラウザ)で、どこで何が動くかが変わります。ルートの ssr の値で、サーバーが受け持つ範囲を選びます。
flowchart TD
A[ブラウザが URL を開く] --> B{ルートの ssr}
B --> C[true<br>描画まで]
B --> D[data-only<br>データだけ]
B --> E[false<br>待ち表示]
C --> F[ハイドレーション]
D --> F
E --> F
F --> G[サーバー関数を<br>RPC で呼ぶ]
よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| TanStack Start とは | TanStack Router を土台にした React のフルスタック・フレームワーク。MIT ライセンスで無料 | TanStack Start 1.0とは |
| TanStack Start 1.0 はいつ出たか | 安定版の発表は 2026年10月13日。RC の発表は 2025年9月23日 | TanStack Start 1.0とは |
| 使い方(始め方) | npx @tanstack/cli@latest create で作り、npm run dev | 最初の5分 |
| サーバー関数の書き方 | createServerFn().validator().handler()。1.169 系は .validator | サーバー関数の書き方 |
| 選択的 SSR とは | ルートごとに ssr: true / 'data-only' / false を選ぶ仕組み | 選択的 SSR |
| RSC に対応しているか | 対応しているが実験的。Vite では @vitejs/plugin-rsc を足して有効にする | React Server Components |
| curl で呼ぶと 403 | CSRF ミドルウェアが Origin などの無い要求を断っている | CSRF 対策 |
| Next.js との違い | Server Action は createServerFn、layout.tsx は __root.tsx などに対応 | Next.js との書き方の違い |
| どちらを選ぶか | URL の状態の型・ルーターを中心に考えるか、RSC を中心に考えるかで分かれる | 判断表 |
確認環境:Ubuntu 24.04.5 LTS・Node.js 22.23.4・npm 10.9.9 の上で、@tanstack/cli 0.72.16 の create --blank で新規プロジェクトを作りました。入った版は @tanstack/react-start 1.169.4・@tanstack/react-router 1.170.46・@tanstack/start-server-core 1.169.45・Vite 8.3.6・@vitejs/plugin-react 6.2.1・React 19.3.1・TypeScript 6.0.3 です。Server Components の確認には @vitejs/plugin-rsc 0.5.39 を足しました。開発サーバー(vite dev)と本番ビルド(vite build → vite preview)はどちらも 127.0.0.1 のポートで起動し、応答は curl 8.5.0、ブラウザの動きは Chromium 153.0.8010.12(playwright 1.63.0)で確かめました。CPU 2コアの共用マシンで、ほかの作業と同時に動かしています。CLI の匿名の利用統計は環境変数で止めた状態です(tanstack telemetry status で「Telemetry disabled」を確認)。1.0 の発表内容・選択的 SSR・サーバー関数・ミドルウェア・Server Components・Next.js からの移行は TanStack の公式ブログと公式ドキュメント、Next.js 側の書き方は Next.js の公式ドキュメントで確かめました。デプロイ先(Cloudflare・Netlify・Vercel など)の画面とアカウントが要る操作は行っていません。図は mermaid 11.16.0 で描画を確かめています(確認日 2026年11月6日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TanStack Start 1.0とは|使い方・サーバー関数・Next.js対応表」
TanStack Start 1.0とは(何が・いつ・誰に・いくらで)
この節では、TanStack Start 1.0 が何で、いつ安定版になり、どんな人向けで、費用がかかるのかが1枚の表で分かります。
公式ドキュメントは TanStack Start を「TanStack Router を土台にしたフルスタックの React フレームワーク」と説明しています。ルーティングは 100% TanStack Router に任せ、その上にドキュメント全体の SSR・ストリーミング・サーバー関数・サーバールート(API)・ミドルウェア・クライアントとサーバーの分割ビルドを足した形です。ビルドには Vite か Rsbuild を使います。
| 項目 | 内容 | 出典 |
|---|---|---|
| 何が | React(と Solid)のフルスタック・フレームワーク。選択的 SSR・型付きのサーバー関数・フルスタックのミドルウェア・環境ごとの関数(createIsomorphicFn) | 公式ブログ「TanStack Start 1.0 is Stable」 |
| いつ | RC の発表は 2025年9月23日、安定版 1.0 の発表は 2026年10月13日 | 公式ブログ(2本) |
| 版番号 | npm の版は 1.x の中で上がり続けている。2026年11月6日時点の latest は 1.169.4 | npm レジストリ(手元で npm view) |
| 誰に | React で SSR とサーバー処理まで1つのプロジェクトで書きたい人。TanStack Router・TanStack Query を使っている人 | 公式ドキュメント Overview |
| いくらで | オープンソース(MIT ライセンス)。フレームワーク自体の利用料は無い。ホスティングの費用は各社の料金 | npm の license 欄 |
| RSC | 実験的な機能として提供。React 19 以上と Vite 8 以上(または Rsbuild 2 以上)が必要 | 公式ドキュメント Server Components |
「1.0」は npm の版番号そのものではありません。手元で npm view @tanstack/react-start time を見ると、RC の発表日(2025年9月23日)に 1.132.0 が、安定版の発表の前日(2026年10月12日 UTC)に 1.169.0 が公開されています。発表の記事は npm の版番号を書いていないので、「1.0 になったのはどの版か」は、この公開日時と後で述べる API の変化から読み取るしかありません。
$ npm view @tanstack/react-start dist-tags
{ beta: '0.0.1-beta.204', alpha: '1.132.0-alpha.25', pre: '1.168.33-pre.0', latest: '1.169.4' }
1.132.0 2025-09-23T17:38:13Z ← RC の発表と同じ日
1.169.0 2026-10-12T23:22:44Z ← 安定版の発表(10月13日)の前日(UTC)
1.169.4 2026-10-22T20:10:55Z ← 2026年11月6日時点の latest
1.0 の発表では、RC からの作業が「仕上げ」より大きかったこと、デプロイ先ごとのアダプターをやめて Vite と Nitro に寄せたこと、Rsbuild に対応したこと、チームのベンチマークで SSR の処理量がおよそ 5.5 倍になったこと(公表値)、npm パッケージに AI エージェント向けの手引き(TanStack Intent)を同梱したことが挙げられています。
最初の5分:npm で新規プロジェクトを作り 127.0.0.1 で動かす
この節では、TanStack Start の使い方の最初の一歩として、新規作成から起動までのコマンドと、その出力・できあがるファイルが分かります。
公式のクイックスタートのコマンドは npx @tanstack/cli@latest create です。対話で、パッケージマネージャーや Tailwind CSS などの追加機能を選びます。ここでは比べやすいように、1ルートだけの最小構成(--blank)を対話なしで作りました。
Node.js の版を確かめる
@tanstack/react-start 1.169.4 の package.json は "engines": { "node": ">=22.12.0" } です。node -v が 22.12 より前なら先に上げます。
プロジェクトを作る
npx @tanstack/cli@latest create app --blank。依存の取得と、ルート一覧の生成のための初回ビルドまで自動で走ります。手元では33秒かかりました。
開発サーバーを起動する
雛形の npm run dev は vite dev --port 3000 です。外から見えないよう、ここでは --host 127.0.0.1 を付けて起動しました。
ルートのファイルを足す
src/routes にファイルを置くと、src/routeTree.gen.ts が自動で書き換わり、リンクの型にもすぐ反映されます。
$ npx @tanstack/cli@latest create app --blank --framework React \
--package-manager npm --no-git --no-intent --no-toolchain --non-interactive
...
dist/server/server.js 92.38 kB │ gzip: 23.42 kB
✓ built in 246ms
◇ Route tree generated
└ Your TanStack app is ready in 'app'.
$ cd app && npx vite dev --host 127.0.0.1 --port 43305 --strictPort
VITE v8.3.6 ready in 689 ms
➜ Local: http://127.0.0.1:43305/
できあがった最小構成のファイルと、Next.js(App Router)で近い役割のファイルは次のとおりです。
| TanStack Start のファイル | 役割 | Next.js で近いもの |
|---|---|---|
| vite.config.ts | tanstackStart() と viteReact() のプラグインを読み込む | next.config.ts |
| src/router.tsx | getRouter() でルーターを作り、型を登録する | (フレームワークの内部) |
| src/routes/__root.tsx | html・head・body の外枠(shellComponent)。HeadContent と Scripts を置く | app/layout.tsx |
| src/routes/index.tsx | 「/」のページ。createFileRoute('/') | app/page.tsx |
| src/routeTree.gen.ts | ルートの一覧と型。自動生成なので手で書き換えない | (無し) |
| src/start.ts | 全体のミドルウェアなどの設定。雛形には無く、必要なときに作る | proxy.ts(旧 middleware.ts)に近い |
依存を入れたあとのプロジェクトの大きさは 131MB(node_modules を含む)でした。Vite の基本は関連記事「React環境構築をViteで行う手順|create viteの使い方」で扱っています。
サーバー関数(createServerFn)の書き方と通信の中身
この節では、TanStack Start のサーバー関数の書き方と、ブラウザから呼んだときに実際に流れるリクエスト・レスポンスが分かります。
サーバー関数は createServerFn() で作り、.handler() にサーバーだけで動く処理を書きます。既定のメソッドは GET で、データを変えるものは { method: 'POST' } にします。入力は .validator() で確かめます。公式ドキュメントは、サーバー関数の定義を .functions.ts、サーバー専用の処理を .server.ts、どちらでも使える型や定数を拡張子なしの .ts に分ける置き方を勧めています。
// src/lib/todos.functions.ts
import { createServerFn } from '@tanstack/react-start'
import { withClientInfo } from './middleware'
import { listTodos, insertTodo } from './todos.server'
export const getTodos = createServerFn({ method: 'GET' }).handler(async () => {
return listTodos()
})
export const addTodo = createServerFn({ method: 'POST' })
.middleware([withClientInfo])
.validator((input: { title: string }) => {
if (typeof input?.title !== 'string' || input.title.trim() === '') {
throw new Error('title は1文字以上の文字列が必要です')
}
return { title: input.title.trim() }
})
.handler(async ({ data, context }) => {
const todo = insertTodo(data.title)
return { ...todo, requestId: context.requestId }
})
呼ぶ側は、ルートの loader からそのまま呼ぶか、コンポーネントの中で useServerFn() に包んで呼びます。データを変えたあとは router.invalidate() で loader を読み直します。
// src/routes/index.tsx
import { createFileRoute, useRouter } from '@tanstack/react-router'
import { useServerFn } from '@tanstack/react-start'
import { getTodos, addTodo } from '../lib/todos.functions'
export const Route = createFileRoute('/')({
loader: () => getTodos(),
component: Home,
})
function Home() {
const todos = Route.useLoaderData()
const router = useRouter()
const add = useServerFn(addTodo)
return (
<main>
<h1>TODO(SSR: true)</h1>
<ul>{todos.map((t) => <li key={t.id}>{t.title}</li>)}</ul>
<button onClick={async () => { await add({ data: { title: '追加したTODO' } }); router.invalidate() }}>追加</button>
</main>
)
}
Chromium でボタンを押したときの通信を記録すると、POST で /_serverFn/<ID> へ送られ、続けて router.invalidate() による GET が出ていました。本文は TanStack Start 独自の直列化の形式(型の印 t と値 s の組)で、ミドルウェアの sendContext で送った clientTime も一緒に入っています。
[req] POST /_serverFn/eyJmaWxlIjoiL3NyYy9saWIvdG9kb3MuZnVuY3Rpb25zLnRz...
accept: application/x-tss-framed, application/x-ndjson, application/json
content-type: application/json
x-tsr-serverfn: true
body: {"t":{"t":10,"i":0,"p":{"k":["data","context"],"v":[{"t":10,"i":1,"p":{"k":["title"],
"v":[{"t":1,"s":"追加したTODO"}]},"o":0},{"t":11,"i":2,"p":{"k":["clientTime"],
"v":[{"t":1,"s":"2026-11-06T04:20:55.482Z"}]},"o":0}]},"o":0},"f":127,"m":[]}
[res] 200 x-tss-serialized: true content-type: application/json vary: Origin
{"t":10,"i":0,"p":{"k":["result"],"v":[{"t":10,"i":1,"p":{"k":["id","title","createdAt","requestId"],
"v":[{"t":0,"s":2},{"t":1,"s":"追加したTODO"},{"t":1,"s":"2026-11-06T04:20:55.496Z"},{"t":1,"s":"f0c7xc"}]},"o":0}]},"o":0}
[req] GET /_serverFn/eyJmaWxlIjoiL3NyYy9saWIvdG9kb3MuZnVuY3Rpb25zLnRz... ← invalidate による getTodos
[dom after] "最初のTODO\n追加したTODO"
1開発時の ID はファイル名が読める
開発サーバーの ID は Base64 で、デコードすると {"file":"/src/lib/todos.functions.ts?tss-serverfn-split","export":"addTodo_createServerFn_handler"} でした。
2本番の ID は64桁の16進数
vite build 後のクライアントのコードでは、ID は64桁の16進数(ハッシュ)に置き換わり、ファイルの場所は読めませんでした。
3処理本体はクライアントに入らない
開発サーバーが返したクライアント用のコードは createClientRpc("…") だけで、validator と handler、todos.server の import は消えていました。
入力の確かめに失敗した場合も、HTTP の状態は 200 のまま、本文にエラーが直列化されて返りました(開発サーバー)。呼び出し側では例外として受け取ります。
HTTP/1.1 200
{"t":10,"i":0,"p":{"k":["error"],"v":[{"t":25,"i":1,"s":{"message":{"t":1,"s":"title は1文字以上の文字列が必要です"}},"c":"$TSR/Error"}]},"o":0}
送れる値は直列化できるものに限られ、POST では FormData も渡せます。Response を返すと、その本文とヘッダーで呼び出しが解決します。リダイレクトは throw redirect({ to: '/login' })、見つからないときは throw notFound() を使います(どちらも @tanstack/react-router から import)。
サーバー関数の型エラーを実際に出す(5例)
この節では、TanStack Start の「型安全なサーバー関数」が具体的にどんな誤りを止めるのかが、tsc --noEmit のエラー原文で分かります。
わざと誤りを5つ入れたファイルを作り、雛形の tsconfig(strict)のまま型検査をしました。6秒で次のエラーが出ました(コードは import の3行を省き、エラーの行番号は元のファイルのものです。TS2345 の途中の2行も省いています)。
// src/typeerr.tsx(誤りを入れた例)
export async function wrong1() {
await addTodo({ data: { title: 123 } }) // 1. 型の違う値
}
export async function wrong2() {
await addTodo({ title: '追加' }) // 2. data で包み忘れ
}
export const wrong3 = createServerFn({ method: 'POST' })
.inputValidator((d: { id: number }) => d) // 3. 以前の版の書き方
.handler(async ({ data }) => data.id)
export const wrong4 = createServerFn().handler(async () => {
return { at: new Date(), fn: () => 1 } // 4. 直列化できない値
})
export function Wrong5() {
return <Link to="/todo">TODO</Link> // 5. 存在しないルート
}
$ npx tsc --noEmit
src/typeerr.tsx(6,27): error TS2322: Type 'number' is not assignable to type 'string'.
src/typeerr.tsx(9,19): error TS2353: Object literal may only specify known properties, and 'title' does not exist in type 'RequiredFetcherDataOptions<readonly [FunctionMiddlewareAfterServer<{}, unknown, undefined, { requestId: string; }, { clientTime: string; }, undefined, undefined>], (input: { title: string; }) => { ...; }>'.
src/typeerr.tsx(12,4): error TS2339: Property 'inputValidator' does not exist on type 'ServerFnBuilder<Register, "POST", true>'.
src/typeerr.tsx(13,21): error TS7031: Binding element 'data' implicitly has an 'any' type.
src/typeerr.tsx(14,48): error TS2345: Argument of type '() => Promise<{ at: Date; fn: () => number; }>' is not assignable to parameter of type 'ServerFn<Register, Method, undefined, undefined, Promise<{ at: Date; fn: () => number; }>, true>'.
Types of property 'fn' are incompatible.
Type '() => number' is not assignable to type 'SerializationError<"Function may not be serializable">'.
src/typeerr.tsx(18,16): error TS2322: Type '"/todo"' is not assignable to type '"/" | "/client-only" | "/data-only" | "/api/hello" | "." | ".."'.
| 誤り | エラー | 読み方と直し方 |
|---|---|---|
| 1. title に数値 | TS2322 | validator の引数の型が、呼び出し側の data の型になる。文字列を渡す |
| 2. data で包み忘れ | TS2353 | 呼び出しは必ず { data: … } の形。addTodo({ data: { title } }) に直す |
| 3. inputValidator | TS2339 | 1.169.4 には無い名前。.validator() に直す |
| 4. 関数を返す | TS2345 | 返り値は直列化できる値だけ。Date は通り、関数は SerializationError になった |
| 5. ルートの綴り違い | TS2322 | リンク先は routeTree.gen.ts の一覧から型で決まる。存在するパスに直す |
3つ目は、ネットの記事のコードを写すときに起こりやすい誤りです。npm から以前の版の型定義を取り出して比べると、@tanstack/start-client-core の 1.132.0・1.150.0・1.160.0・1.170.0 の createServerFn.d.ts には inputValidator があり、@tanstack/react-start 1.169.0 が依存する 1.170.36 からは validator に変わっていました。RC 期の記事の .inputValidator(...) は、1.169 系では .validator(...) に読み替えます。
ミドルウェアの書き方(リクエスト用とサーバー関数用)
この節では、TanStack Start のミドルウェアの2つの種類と、クライアントからサーバーへ値を渡す sendContext の動きが分かります。
リクエストミドルウェア(既定)
createMiddleware().server(...)
SSR・サーバールート・サーバー関数のすべての要求で動く
ログ・認証の入口・CSRF の確認に向く
サーバー関数ミドルウェア
createMiddleware({ type: 'function' })
.client() と .server() の両方を書ける
サーバー関数だけに付く。.validator() も持てる
// src/lib/middleware.ts
import { createMiddleware } from '@tanstack/react-start'
export const requestLogger = createMiddleware().server(async ({ next, request }) => {
const t0 = performance.now()
const result = await next()
console.log(`[req] ${request.method} ${new URL(request.url).pathname} ${(performance.now() - t0).toFixed(1)}ms`)
return result
})
export const withClientInfo = createMiddleware({ type: 'function' })
.client(async ({ next }) => {
return next({ sendContext: { clientTime: new Date().toISOString() } })
})
.server(async ({ next, context }) => {
console.log('[fn] clientTime from client =', context.clientTime)
return next({ context: { requestId: Math.random().toString(36).slice(2, 8) } })
})
ブラウザのボタンから呼んだときと、curl で直接 POST したときで、サーバーのログは次のように変わりました。.client() の部分はブラウザで動くので、curl から呼ぶと clientTime は届きません。
[fn] clientTime from client = 2026-11-06T04:20:55.482Z ← ブラウザのボタンから
[fn] clientTime from client = undefined ← curl から直接 POST
公式ドキュメントも、クライアントから送られた値は自動では確かめられないので、サーバー側で形を確かめ、セッションの利用者がその値を使ってよいかを確かめるよう書いています。sendContext の値を権限の判断に使わないことが大切です。全体に掛けるミドルウェアは src/start.ts の createStart に requestMiddleware(すべての要求)と functionMiddleware(すべてのサーバー関数)として並べ、実行順は「全体 → 依存されている側 → 個別」です。
CSRF 対策:src/start.ts を作ると既定の守りが外れる
この節では、サーバー関数を curl で呼ぶと 403 Forbidden になる理由と、src/start.ts を作ったときに起きる落とし穴が、実際の応答で分かります。
公式ドキュメントによると、サーバー関数は同じオリジンから呼ぶ RPC の窓口で、Sec-Fetch-Site・Origin・Referer のどれも無い要求は既定で断られます。src/start.ts が無いときは、Start が CSRF ミドルウェアを自動で入れます。開発サーバー(上の3行)と本番ビルド(下の3行)で、ヘッダーを変えて POST した結果は次のとおりです。
| src/start.ts | 送ったヘッダー | 結果 |
|---|---|---|
| 無し(既定) | Origin などを付けない | 403 Forbidden |
| 無し(既定) | Origin: http://evil.example | 403 Forbidden |
| 無し(既定) | Origin: 自分のオリジン | 200(TODO が追加された) |
| あり・CSRF を入れ忘れ | Origin などを付けない | 200(追加されてしまった) |
| あり・filter 付きで追加 | Origin などを付けない | 403 Forbidden |
| あり・filter 付きで追加 | Sec-Fetch-Site: cross-site | 403 Forbidden |
ログを取るために src/start.ts を作り、requestMiddleware に自分のミドルウェアだけを並べると、CSRF の確認が外れて、Origin の無い POST が通りました。開発サーバーでは、このとき次の警告が出ます(1回だけ)。
TanStack Start server functions are not protected by the CSRF middleware.
Server functions are same-origin RPC endpoints and should be protected from cross-site requests.
Add the CSRF middleware in src/start.ts:
const csrfMiddleware = createCsrfMiddleware({
filter: (ctx) => ctx.handlerType === 'serverFn',
})
CSRF ミドルウェアの入れ方
requestMiddleware: [createCsrfMiddleware(), ...](filter なし)。手元ではページの GET(/ や /rsc)まで 403 になり、Chromium でもページが開けなかったcreateCsrfMiddleware({ filter: (ctx) => ctx.handlerType === 'serverFn' }) を入れる。ページは 200、Origin の無い POST だけ 403 になった見分ける問い:「確認をかけたいのはサーバー関数だけか、すべての要求か」
// src/start.ts(直したあと)
import { createCsrfMiddleware, createStart } from '@tanstack/react-start'
import { requestLogger } from './lib/middleware'
const csrfMiddleware = createCsrfMiddleware({
filter: (ctx) => ctx.handlerType === 'serverFn',
})
export const startInstance = createStart(() => ({
requestMiddleware: [csrfMiddleware, requestLogger],
}))
$ curl -i -X POST http://127.0.0.1:43306/_serverFn/9ab9e82b...a677 \
-H 'content-type: application/json' -H 'x-tsr-serverfn: true' -d '…'
HTTP/1.1 403
Forbidden
$ curl -i -X POST ... -H 'Origin: http://127.0.0.1:43306' -d '…'
HTTP/1.1 200
{"t":10,"i":0,"p":{"k":["result"],"v":[{"t":10,"i":1,"p":{"k":["id","title","createdAt","requestId"], ...
CSRF ミドルウェアは同じサイトの外からの送信を止めるもので、ログインしているかどうかは見ません。公式ドキュメントは、個人のデータを読み書きするサーバー関数には認証のミドルウェア(または handler の中の確認)を付けること、ルートの beforeLoad はデータを守る境界にならないことを書いています。CSRF そのものの仕組みは関連記事「CSRF対策とSameSite|Cookie送信をChromeで実測し多層防御を実装」で扱っています。
選択的 SSR(ssr: true・'data-only'・false)の違いを HTML で確かめる
この節では、TanStack Start の選択的 SSR の3つの値で、サーバーが返す HTML がどう変わるかが、本番ビルドの実物で分かります。
同じ loader(TODO の一覧を返すサーバー関数)を持つルートを3つ作り、ssr だけを変えました。pendingComponent は、サーバーが描画しないときの待ち表示です。
// src/routes/data-only.tsx(client-only.tsx は ssr: false にしただけ)
export const Route = createFileRoute('/data-only')({
ssr: 'data-only',
loader: () => getTodos(),
pendingComponent: () => <p>読み込み中(data-only)</p>,
component: () => <p>件数: {Route.useLoaderData().length}(data-only)</p>,
})
| ssr の値 | HTML の body の先頭 | HTML 内のデータ | ブラウザでの loader |
|---|---|---|---|
| true(既定) | <main><h1>TODO(SSR: true)… 描画済み | あり(2か所) | 動かない |
| 'data-only' | <p>読み込み中(data-only)</p> | あり(1か所) | 動かない |
| false | <p>読み込み中(ssr: false)</p> | 無し(0か所) | GET /_serverFn/… が出た |
「HTML 内のデータ」は、TODO の文字列が HTML に何回出るかを数えたものです。true は描画した部分とハイドレーション用のデータの2か所、'data-only' はデータの1か所だけ、false は0か所でした。Chromium で開くと、'data-only' は追加の通信なしで「件数」が表示され、false はブラウザから getTodos の GET が出てから表示されました。
ssr: true
検索エンジンや SNS のプレビューに中身を見せたいページ。サーバーで描画まで行う。
既定の値
ssr: 'data-only'
データはサーバーで取りたいが、描画に window などブラウザの機能が要るページ(グラフ・地図)。
データだけ先に届く
ssr: false
ログイン後の管理画面など、SSR の利点が小さいページ。サーバーの負担も小さい。
SPA と同じ動き
子のルートは親の値を引き継ぎ、引き継いだ値は「より制限の強い方向」にだけ変えられます(true → 'data-only' → false)。親が false なら、子に true と書いても false のままです。ssr には関数も書け、初回の要求のときにだけサーバーで評価されます(クライアントのコードからは取り除かれます)。全体の既定は createStart の defaultSsr で変えられます。
React Server Components 対応(実験的)を動かす
この節では、TanStack Start の React Server Components 対応がどの段階か、どう有効にするか、手元で動かした結果が分かります。
公式ドキュメントでは Server Components は実験的な機能で、API はまだ変わりうると書かれています。既定では無効で、Vite では @vitejs/plugin-rsc を足し、tanstackStart({ rsc: { enabled: true } }) にします。React 19 以上と、Vite 8 以上(または Rsbuild 2 以上)が必要です。
// vite.config.ts(RSC を有効にした形)
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'
import rsc from '@vitejs/plugin-rsc'
export default defineConfig({
resolve: { tsconfigPaths: true },
plugins: [tanstackStart({ rsc: { enabled: true } }), rsc(), viteReact()],
})
// src/routes/rsc.tsx
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
import { renderServerComponent } from '@tanstack/react-start/rsc'
async function ServerTime() {
const { listTodos } = await import('../lib/todos.server')
return <p>サーバーで描画:TODO {listTodos().length} 件({new Date().toISOString()})</p>
}
const getServerTime = createServerFn().handler(async () => {
const Renderable = await renderServerComponent(<ServerTime />)
return { Renderable }
})
export const Route = createFileRoute('/rsc')({
loader: async () => ({ ServerTime: (await getServerTime()).Renderable }),
component: () => <main>{Route.useLoaderData().ServerTime}</main>,
})
ビルドは rsc・ssr・client の3つの環境に分かれ、4.6秒で終わりました(下の HTML は React が入れる区切りの印を省いています)。/rsc は 200 で、サーバーで描画された文がそのまま HTML に入り、Chromium でも同じ文が表示されました。
vite v8.3.6 building rsc environment for production...
dist/server/rsc/index.js 177.99 kB │ gzip: 37.97 kB
vite v8.3.6 building client environment for production...
dist/client/assets/index-CuC314xO.js 254.88 kB │ gzip: 80.75 kB
$ curl http://127.0.0.1:43306/rsc
... <main><p>サーバーで描画:TODO 1 件(2026-11-06T04:25:44.615Z)</p></main> ...
[hydrated] /rsc サーバーで描画:TODO 3 件(2026-11-06T04:25:54.548Z)
Next.js の App Router はページとレイアウトが既定で Server Components で、ブラウザで動かす部品に 'use client' を付けます。TanStack Start は逆に、ふだんのルートは SSR+ハイドレーションで、Server Components は renderServerComponent などで使う場所を選んで足す形です。公式ドキュメントは、スロットの中身をサーバー側で React.Children.map() できないこと、TanStack Query に入れるときは structuralSharing: false にすることなどの制約も挙げています。
サーバーとクライアントの境界:import protection と createIsomorphicFn
この節では、サーバー専用のコードを誤ってブラウザ側から読み込んだときのビルドエラーと、環境ごとに処理を分ける書き方が分かります。
TanStack Start は、ルートのファイルが既定でサーバーとブラウザの両方で動きます。.server.ts のファイルをコンポーネントから直接 import すると、vite build が次のエラーで止まりました。
[plugin tanstack-start-core:import-protection]
RolldownError:
[import-protection] Import denied in client environment
Denied by file pattern: **/*.server.*
Importer: src/routes/leak.tsx:5:24
Import: "../lib/todos.server"
Resolved: src/lib/todos.server.ts
Trace:
1. src/router.tsx:2:28 (entry) (import "./routeTree.gen")
2. src/routeTree.gen.ts:5:43 (import "./routes/leak")
3. src/routes/leak.tsx:5:24 (import "../lib/todos.server")
Suggestions:
- Use createServerFn().handler(() => ...) to keep the logic on the server and call it from the client via an RPC bridge
- Use createServerOnlyFn(() => ...) to mark it as server-only (it will throw if accidentally called from the client)
- Use createIsomorphicFn().client(() => ...).server(() => ...) to provide separate client and server implementations
- Move the server-only import out of this file into a separate .server.ts module that is not imported by any client code
エラーには、どのファイルからどの順で読み込まれたか(Trace)と、直し方の候補が4つ出ます。Next.js では import 'server-only' を書いたモジュールが同じ役割を持ちますが、TanStack Start ではファイル名の .server. で決まります。環境ごとに中身を変えたいときは、1.0 の発表でも例に出ていた createIsomorphicFn を使います。合わない側の実装はそれぞれのバンドルから取り除かれます。
import { createIsomorphicFn } from '@tanstack/react-start'
import { getRequest } from '@tanstack/react-start/server'
export const getOrigin = createIsomorphicFn()
.server(() => new URL(getRequest().url).origin)
.client(() => window.location.origin)
本番ビルドのクライアントのコードを grep すると、ミドルウェアの .server() に書いたログの文字列は0件、.client() の sendContext は1件、todos.server.ts の中身(「最初のTODO」)は0件でした。
ビルドと本番起動・実測(ビルド時間・大きさ・応答時間)
この節では、TanStack Start の本番ビルドにかかる時間、できあがるファイルの大きさ、127.0.0.1 での応答時間の目安が分かります。
$ npx vite build
vite v8.3.6 building client environment for production...
✓ 156 modules transformed.
dist/client/assets/data-only-CMNLgRdL.js 0.16 kB │ gzip: 0.18 kB
dist/client/assets/client-only-CJj2Mclo.js 0.17 kB │ gzip: 0.18 kB
dist/client/assets/routes-B8BRRfkY.js 0.69 kB │ gzip: 0.45 kB
dist/client/assets/index-D42JoGB7.js 344.68 kB │ gzip: 111.64 kB
✓ built in 628ms
vite v8.3.6 building ssr environment for production...
dist/server/assets/todos.functions-DxIdmbWV.js 1.91 kB │ gzip: 0.94 kB
dist/server/server.js 99.23 kB │ gzip: 25.23 kB
✓ built in 252ms
real 0m2.431s
Node.js の fetch で同じ URL を逐次300回(先に20回の慣らし)取り、かかった時間を並べました。1本の接続で順に送っただけの目安で、ほかの作業と同時に動く2コアのマシンの数字です。ほかのフレームワークとの比べ方には使えません。
| URL | HTML の大きさ | 本番 p50 / p95 | 開発 p50 / p95 |
|---|---|---|---|
| /(ssr: true) | 2,243 バイト | 2.08 / 8.52 ms | 4.95 / 24.35 ms |
| /data-only | 2,244 バイト | 2.84 / 10.54 ms | 4.14 / 7.48 ms |
| /client-only(ssr: false) | 2,162 バイト | 1.46 / 4.45 ms | 3.61 / 7.76 ms |
| /api/hello(サーバールート) | 29 バイト | 0.94 / 3.81 ms | 1.83 / 5.76 ms |
ssr: false のページはサーバーで loader も描画もしないぶん速く返りますが、その後ブラウザがサーバー関数を呼ぶので、画面に中身が出るまでの時間は別に考えます。本番の起動は、この記事では vite preview を使いました。実際のデプロイは、1.0 の発表のとおり Vite と Nitro の仕組みで各社の環境に合わせる形で、CLI の --deployment には cloudflare・netlify・nitro・railway・render・vercel が並んでいます(各社の設定は公式ドキュメントの範囲)。
サーバールートは、ルートのファイルに server.handlers を書いて作ります。ミドルウェアも付けられます。
// src/routes/api.hello.ts
export const Route = createFileRoute('/api/hello')({
server: {
middleware: [requestLogger],
handlers: {
GET: ({ request }) => Response.json({ hello: 'world', ua: request.headers.get('user-agent') }),
},
},
})
Next.js(App Router)との書き方の違い(対応表)
この節では、Next.js の App Router で書いていたものを TanStack Start でどう書くかが、対応表で分かります。どちらが優れているかではなく、書き方の対応です。
TanStack Start の公式ドキュメントには Next.js からの移行の手引きがあり、次の対応はそこから取りました。Next.js 側の書き方は Next.js の公式ドキュメントで確かめています。
| やりたいこと | Next.js(App Router) | TanStack Start |
|---|---|---|
| 外枠のレイアウト | app/layout.tsx | src/routes/__root.tsx(HeadContent と Scripts を置く) |
| トップページ | app/page.tsx | src/routes/index.tsx |
| 動的なパス | app/posts/[slug]/page.tsx | src/routes/posts.$slug.tsx |
| 残り全部のパス | app/docs/[...parts]/page.tsx | src/routes/docs.$.tsx |
| API | app/api/hello/route.ts | src/routes/api.hello.ts(server.handlers) |
| サーバーで動く関数 | 'use server'(Server Functions・Server Actions) | createServerFn と useServerFn |
| 更新後の読み直し | revalidatePath・revalidateTag | router.invalidate()(CDN のキャッシュは消さない) |
| リンク | next/link | Link(params まで型がつく) |
| 検索パラメーター | searchParams | validateSearch と Route.useSearch() |
| メタデータ | metadata・generateMetadata | ルートの head(loader の結果からも作れる) |
| 画像の最適化 | next/image | 組み込みは無い。img か画像サービス |
同じ「TODO を1件追加する」処理を並べると、違いは「どこに印を付けるか」と「呼び方」です。
// Next.js(App Router): app/actions.ts
'use server'
export async function addTodo(formData: FormData) {
const title = String(formData.get('title') ?? '')
// DB に書く
revalidatePath('/')
}
// app/page.tsx(Server Component)
// <form action={addTodo}><input name="title" /><button>追加</button></form>
// TanStack Start: src/lib/todos.functions.ts
export const addTodo = createServerFn({ method: 'POST' })
.validator((input: { title: string }) => input)
.handler(async ({ data }) => insertTodo(data.title))
// src/routes/index.tsx(コンポーネント)
// const add = useServerFn(addTodo); await add({ data: { title } }); router.invalidate()
移行の手引きは、createServerFn は React のフォームの Action の仕組みをそのまま置き換えるものではない、と注意しています。<form action={…}> で書いた画面は、送信の処理を書き直す前提で見積もります。App Router の全体像は関連記事「Next.js App Router 入門|v16対応・実際のビルド結果つき」で扱っています。Server Actions の書き方と安全対策は関連記事「Next.js Server Actionsの使い方|CRUDテンプレートと安全対策」で扱っています。
データの取得とキャッシュを TanStack Query に任せる構成は、RC の発表で「先読み・キャッシュ・ハイドレーションでの深い統合」として挙げられ、公式の例にも「Basic + React Query」(start-basic-react-query)があります。useQuery・useMutation の基本は関連記事「TanStack Query v5の使い方|useQuery・useMutation」で扱っています。
よくあるエラー/つまずき
この節では、TanStack Start を動かすときに出やすいエラーの原文と、原因・直し方が一覧で分かります。
| 症状・エラー原文 | 原因 | 直し方 |
|---|---|---|
curl でサーバー関数を呼ぶと 403 Forbidden | CSRF ミドルウェアが Origin などの無い要求を断った | ブラウザから呼ぶ。検証では Origin を付ける。外部から呼ぶ API はサーバールートで作る |
| ページの GET まで 403 | filter なしの createCsrfMiddleware() を requestMiddleware に入れた | filter: (ctx) => ctx.handlerType === 'serverFn' を付ける |
server functions are not protected by the CSRF middleware | src/start.ts を作り、CSRF ミドルウェアを入れていない | 警告の例のとおり filter 付きで追加する |
Property 'inputValidator' does not exist | RC 期の書き方 | .validator() に直す |
expected GET method. Got POST(405) | GET のサーバー関数に POST した | createServerFn の method と呼び方を合わせる |
Import denied in client environment | .server.ts をブラウザ側のコードから import した | サーバー関数の handler の中だけで使う |
SerializationError<"Function may not be serializable"> | 関数などをサーバー関数から返した | 直列化できる値(文字列・数値・Date・配列など)にする |
A notFoundError was encountered on the route with ID "__root__" | notFoundComponent を設定していない状態で、ルートに無いパスが要求された | ルートかルーターに notFoundComponent を設定する |
最後の警告は、ルートに無いパスへの要求で出ます。手元では、Chromium でページを開いたときのブラウザ側の 404 と同じタイミングで出ていました。開発サーバーの「Re-optimizing dependencies because lockfile has changed」は、依存を足したあとの初回起動で出るもので、エラーではありません。
- 1自分で確かめる:入っている版を見るターミナルで
npm ls @tanstack/react-start @tanstack/start-server-core。手元は 1.169.4 と 1.169.45 だった - 2自分で確かめる:サーバー関数の通信を見るブラウザの開発者ツールの「ネットワーク」で
_serverFnと絞り込み、メソッド・x-tsr-serverfn ヘッダー・応答の本文を見る - 3自分で確かめる:SSR されているかを見る
curl -s http://127.0.0.1:3000/ | grep -o '<main>.\{0,80\}'で、本文が HTML に入っているかを見る - 4セキュリティの更新を追う公式ブログは 2026年9月30日に CVE-2026-102989 を公表し、start-server-core 1.169.39 以上への更新と再デプロイを求めている
判断表:TanStack Start が合う場面・Next.js を続ける場面
この節では、新しく始めるとき・今の Next.js から移るときに、TanStack Start を選ぶかどうかの判断のしかたが分かります。どちらかが優れているという話ではなく、何を中心に設計するかの違いです。
- 1今の Next.js で困っていない → 続ける移行は Server Action・キャッシュ・画像の置き換えが要る
- 2RSC を設計の中心にしたい → Next.js の App RouterTanStack Start の RSC は2026年11月6日時点で実験的
- 3TanStack Router の SPA に SSR とサーバー処理を足したい → TanStack Startルーティングはそのまま使える
- 4URL の状態(検索・絞り込み)に型を付けたい → TanStack StartvalidateSearch と Link の params が型で守られる
- 5ページごとに SSR・SPA を混ぜたい → どちらでも可。Start は ssr の値で切り替え管理画面は false、公開ページは true など
- 6ビルドを Vite(または Rsbuild)にそろえたい → TanStack Startvite.config.ts のプラグインで動く
| 場面 | 向く選択 | 理由 |
|---|---|---|
| 社内の管理画面を新しく作る | TanStack Start(ssr: false を多用)か SPA | SSR の利点が小さく、型付きのルーターとサーバー関数が効く |
| 公開サイト・ブログ・EC の一覧 | どちらでも可 | どちらも SSR とメタデータを持つ。チームの経験で選ぶ |
| Next.js の大きな既存サイト | Next.js を続ける | Server Action・ISR・next/image の置き換えの費用が大きい |
| TanStack Router+Query の SPA | TanStack Start | ルートの書き方を変えずにサーバー処理を足せる |
| Server Components を広く使いたい | Next.js の App Router | Start の RSC は実験的で、API が変わる可能性がある |
よくある質問
Q. TanStack Start とは何ですか? A. TanStack Router を土台にした React(と Solid)のフルスタック・フレームワークです。SSR・ストリーミング・型付きのサーバー関数・サーバールート・ミドルウェアを持ち、Vite か Rsbuild でビルドします。
Q. TanStack Start 1.0 はいつ安定版になりましたか? A. 公式ブログ「TanStack Start 1.0 is Stable」は 2026年10月13日付です。RC の発表は 2025年9月23日でした。npm では 1.169.0 が 2026年10月12日(UTC)に公開されています。
Q. 本番で使ってよいですか?
A. 1.0 の発表で「安定版で本番に使える」とされています。ただし Server Components は実験的です。2026年9月30日に公表された CVE-2026-102989 のように更新が出るので、npm ls @tanstack/start-server-core で 1.169.39 以上かを確かめ、更新したら再デプロイします。
Q. 料金はかかりますか? A. MIT ライセンスのオープンソースで、フレームワークの利用料はありません。費用が出るのは、デプロイ先のホスティングやデータベースなど各社のサービスです。
Q. TanStack Router だけと何が違いますか? A. Router は SPA のルーターで、Start はその上に SSR・サーバー関数・サーバールート・ミドルウェアを足したものです。公式ドキュメントは、これらが要らないと決まっているなら Router だけでもよい、としています。
Q. サーバー関数を curl や別のアプリから呼べますか?
A. 既定の CSRF ミドルウェアにより、Origin などの無い要求は 403 になりました。サーバー関数は同じオリジンから呼ぶ RPC の窓口なので、外部から呼ぶ API は server.handlers のサーバールートで作ります。
Q. inputValidator と validator はどちらを使いますか?
A. @tanstack/react-start 1.169.4 では .validator() です。.inputValidator() は TS2339 になりました。RC 期の記事のコードは読み替えが要ります。
Q. React Server Components は使えますか?
A. 実験的な機能として使えます。@vitejs/plugin-rsc を足して tanstackStart({ rsc: { enabled: true } }) にし、renderServerComponent などを使います。手元の /rsc のルートは本番ビルドで 200 を返しました。
Q. 開発サーバーのポートは何番ですか?
A. 雛形の npm run dev は vite dev --port 3000 でした。外から見えないようにするなら --host 127.0.0.1 を付けます。
Q. Next.js から移るとき、いちばん手間がかかるのはどこですか? A. 公式の手引きが「そのまま置き換えられない」と書いているのは、フォームの Server Action、revalidatePath などのキャッシュ、next/image です。ルートのファイルの置き換えは表のとおり機械的にできます。
まとめ
- TanStack Start は TanStack Router を土台にした React のフルスタック・フレームワークで、1.0 の安定版は 2026年10月13日に発表された。MIT ライセンスで無料。
- サーバー関数は
createServerFn().validator().handler()。呼び出しのdataの型・返り値の直列化・リンク先まで型検査で止まった。1.169 系は.validator。 - 選択的 SSR は
ssr: true / 'data-only' / false。HTML に入るものが「描画とデータ」「データだけ」「待ち表示だけ」と変わるのを実物で確かめた。 src/start.tsを作ると CSRF の守りが外れる。filter 付きのcreateCsrfMiddlewareを入れると、ページは 200、Origin の無い POST は 403 になった。- Next.js との違いは「どこに印を付け、どう呼ぶか」。RSC を中心にするか、型付きのルーターを中心にするかで選ぶ。
参考・出典
以下はすべて確認日 2026年11月6日です。
- TanStack Blog「TanStack Start 1.0 is Stable」(2026年10月13日):https://tanstack.com/blog/tanstack-start-1-0
- TanStack Blog「Announcing TanStack Start v1 Release Candidate」(2025年9月23日):https://tanstack.com/blog/announcing-tanstack-start-v1
- TanStack Blog「TanStack Start security update: CVE-2026-102989」(2026年9月30日):https://tanstack.com/blog/tanstack-start-security-update-cve-2026-102989
- TanStack Start 公式ドキュメント「Overview」:https://tanstack.com/start/latest/docs/framework/react/overview
- TanStack Start 公式ドキュメント「Quick Start」:https://tanstack.com/start/latest/docs/framework/react/quick-start
- TanStack Start 公式ドキュメント「Server Functions」:https://tanstack.com/start/latest/docs/framework/react/guide/server-functions
- TanStack Start 公式ドキュメント「Middleware」:https://tanstack.com/start/latest/docs/framework/react/guide/middleware
- TanStack Start 公式ドキュメント「Selective SSR」:https://tanstack.com/start/latest/docs/framework/react/guide/selective-ssr
- TanStack Start 公式ドキュメント「Server Components」:https://tanstack.com/start/latest/docs/framework/react/guide/server-components
- TanStack Start 公式ドキュメント「Migrate from Next.js」:https://tanstack.com/start/latest/docs/framework/react/migrate-from-next-js
- npm「@tanstack/react-start」:https://www.npmjs.com/package/@tanstack/react-start
- Next.js 公式ドキュメント「Server and Client Components」:https://nextjs.org/docs/app/getting-started/server-and-client-components
- Next.js 公式ドキュメント「How to update data」:https://nextjs.org/docs/app/getting-started/updating-data
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TanStack Start 1.0とは|使い方・サーバー関数・Next.js対応表」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。