---
title: TanStack StartのcreateServerFnで作る、型が通るAPI
tags: 
author: [Yuki Terashima](https://image.docswell.com/user/yt4)
site: [Docswell](https://www.docswell.com/)
thumbnail: https://bcdn.docswell.com/page/PJR9PRDL79.jpg?width=480
description: TSKaigi 2026のLT登壇資料です。  https://2026.tskaigi.org/talks/25
published: May 22, 26
canonical: https://image.docswell.com/s/yt4/Z8NMGQ-tskaigi-2026-create-server-fn
---
# Page. 1

![Page Image](https://bcdn.docswell.com/page/PJR9PRDL79.jpg)

TanStack Start の
createServerFn で作る、
型が通る API
TSKaigi 2026 DAY1 16:40 ~ 16:50｜ #tskaigi_righttouch ｜ Yuki Terashima


# Page. 2

![Page Image](https://bcdn.docswell.com/page/PEXQ3R26JX.jpg)

自己紹介
Yuki Terashima（@y_temp4）
フリーランスのフロントエンドエンジニア
React / TypeScript
TSKaigi 2026 / TanStack Start の createServerFn で作る、型が通る API
2 / 13


# Page. 3

![Page Image](https://bcdn.docswell.com/page/3EK9YRMGED.jpg)

LT で話すこと
で 型は通る。
でも API 設計 はどうする？
createServerFn
型が通ることと、境界が設計されていることは別です。
TSKaigi 2026 / TanStack Start の createServerFn で作る、型が通る API
3 / 13


# Page. 4

![Page Image](https://bcdn.docswell.com/page/L73W98Y575.jpg)

createServerFn の使い方
import { z } from &quot;zod&quot;;
export const createTodo = createServerFn({ method: &quot;POST&quot; })
.inputValidator(z.object({ title: z.string().min(1) }))
.handler(async ({ data }) =&gt; {
return await db.todo.create({ data });
});
コンポーネントなどから呼び出せる
// React
await createTodo({ data: { title: &quot;LT
資料を書く&quot; } });
server-only な処理を、クライアントから呼べる
引数は data として渡す
inputValidator で Runtime Validation できる
TSKaigi 2026 / TanStack Start の createServerFn で作る、型が通る API
4 / 13


# Page. 5

![Page Image](https://bcdn.docswell.com/page/87DKGM5YJG.jpg)

何が壊れやすいのか
handler が太る
境界がロジック置き場になる
単体テストしづらくなる
再利用しづらくなる
出力の境界が曖昧になる
返してよい field が不明確
内部用の値が混ざる
クライアント向けの型として
不安定になる
責務が混ざる
loader が DB 取得の
実装を持つ
serverFn が業務ロジックの
本体になる
コンポーネントが
DB 由来の型に依存する
型が通ることと、境界が安定していることは別
TSKaigi 2026 / TanStack Start の createServerFn で作る、型が通る API
5 / 13


# Page. 6

![Page Image](https://bcdn.docswell.com/page/VJPK3RG2E8.jpg)

指針 1: serverFn は薄くする
NG: handler に全部書く
export const createTodo = createServerFn({
method: &quot;POST&quot;,
}).handler(async ({ data, context }) =&gt; {
//
// validation
//
// DB
//
// ...
});
認可
ビジネスルール
操作
通知
OK: 境界に徹する
export const createTodo = createServerFn({
method: &quot;POST&quot;
}).inputValidator(createTodoInputSchema)
.handler(async ({ data }) =&gt; {
return todoOutputSchema.parse(
await createTodoUseCase(data),
);
});
serverFn は 境界、UseCase は 業務ロジック
UseCase を直接テストできる形にする
TSKaigi 2026 / TanStack Start の createServerFn で作る、型が通る API
6 / 13


# Page. 7

![Page Image](https://bcdn.docswell.com/page/2EVV4R6XEQ.jpg)

指針 2: 出力も Schema 化する
const todoOutputSchema = z.object({
id: z.string(),
title: z.string(),
done: z.boolean(),
createdAt: z.number().int(),
});
export const createTodo = createServerFn({ method: &quot;POST&quot; })
.inputValidator(createTodoInputSchema)
.handler(async ({ data }) =&gt; {
const row = await createTodoUseCase(data);
return todoOutputSchema.parse(row);
});
推論に頼らず、出力を Schema 化する
型安全でも返すべきデータとは限らない
TSKaigi 2026 / TanStack Start の createServerFn で作る、型が通る API
7 / 13


# Page. 8

![Page Image](https://bcdn.docswell.com/page/57GL1MNREL.jpg)

指針 3 (1/2): loader は server-only ではない
TanStack Start の loader は isomorphic
SSR 時はサーバー、ページ遷移時はクライアントで動く
DB / secret は loader ではなく serverFn に閉じ込める
を
とみなして
に触っている
// NG: loader
server-only
DB
export const Route = createFileRoute(&quot;/todos&quot;)({
loader: async () =&gt; {
const session = readSessionCookie();
return await db.todo.findMany({
where: { userId: session.userId },
});
},
});
loader
は Route とデータ取得をつなぐ場所。DB や認可は serverFn に閉じ込める
TSKaigi 2026 / TanStack Start の createServerFn で作る、型が通る API
8 / 13


# Page. 9

![Page Image](https://bcdn.docswell.com/page/4EQYDRQYJP.jpg)

指針 3 (2/2): Read も serverFn に切り出す
layer
役割
Route のデータ依存関係を定義
server-only 処理の RPC 境界
業務ロジック本体
loader
serverFn
UseCase
も
に切り出し、
置くもの
params / search / 初期表示
コンテキスト取得 / 入出力 Schema / UseCase 呼び出し
認可チェック / DB 操作 / ビジネスルール
はそれを呼ぶだけ
// OK: read
serverFn
loader
export const listTodos = createServerFn({ method: &quot;GET&quot; }).handler(async () =&gt; {
return todosOutputSchema.parse(await listTodosUseCase());
});
export const Route = createFileRoute(&quot;/todos&quot;)({
loader: () =&gt; listTodos(),
});
serverFn
は Write 専用ではない。DB に触る Read も createServerFn を使う。
TSKaigi 2026 / TanStack Start の createServerFn で作る、型が通る API
9 / 13


# Page. 10

![Page Image](https://bcdn.docswell.com/page/KJ4WZ8NZ71.jpg)

ファイル分割例：責務で 3 層に分ける
todos/
schemas.ts
listTodos.server.ts
createTodo.server.ts
...
functions.ts
functions.ts
ファイル
責務
schemas.ts
入出力のスキーマ定義
*.server.ts [1]
業務ロジック本体
functions.ts
createServerFn
の RPC 境界
は実装本体ではなく RPC 境界
1. *.server.ts はクライアントから import できない。参考：Import Protection ↩︎
TSKaigi 2026 / TanStack Start の createServerFn で作る、型が通る API
10 / 13


# Page. 11

![Page Image](https://bcdn.docswell.com/page/LE1YR25D7G.jpg)

コロケーション vs バックエンド分離
Route 近くに置く[1]
server/
src/routes/todos/
route.tsx
-api/
schemas.ts
listTodos.server.ts
createTodo.server.ts
...
functions.ts
画面と処理の対応を追いやすい
に寄せる
src/server/todos/
schemas.ts
listTodos.server.ts
createTodo.server.ts
...
functions.ts
複数 Route から使いやすい
サーバー処理がどこにあるか一目で分かる
置き場所を変えても schemas / 業務ロジック / functions の責務は混ぜない
1. 参考実装: https://github.com/y-temp4/create-server-fn-sample ↩︎
TSKaigi 2026 / TanStack Start の createServerFn で作る、型が通る API
11 / 13


# Page. 12

![Page Image](https://bcdn.docswell.com/page/GEWG1R58J2.jpg)

補足：他の API スタイルとの位置付け
観点
型の共有
HTTP 境界
外部公開 API 向き
アプリ内部 RPC
REST
弱
明示
◎
△
GraphQL
中
明示
◎
△
tRPC
強
RPC で抽象化
△
◎
createServerFn
強
隠蔽寄り
△
◎
は 同一アプリ内の RPC 境界
Webhook / Mobile Client / 外部連携には、明示的な HTTP API が必要
TanStack Start なら Server Routes、一般には REST / OpenAPI などを検討する
createServerFn
TSKaigi 2026 / TanStack Start の createServerFn で作る、型が通る API
12 / 13


# Page. 13

![Page Image](https://bcdn.docswell.com/page/47ZLPRN9J3.jpg)

まとめ
の価値は エンドポイント定義を省略できること だけではない
本質は クライアントとサーバーの境界に型付きの契約を置くこと
契約を壊れにくくするために
1. serverFn は 薄く 保つ
2. 入力 と出力 を Schema 化する
3. loader と serverFn の責務を分ける
createServerFn


