799 Views
May 22, 26
スライド概要
TSKaigi 2026のLT登壇資料です。
https://2026.tskaigi.org/talks/25
https://y-temp4.com
TanStack Start の createServerFn で作る、 型が通る API TSKaigi 2026 DAY1 16:40 ~ 16:50| #tskaigi_righttouch | Yuki Terashima
自己紹介 Yuki Terashima(@y_temp4) フリーランスのフロントエンドエンジニア React / TypeScript TSKaigi 2026 / TanStack Start の createServerFn で作る、型が通る API 2 / 13
LT で話すこと で 型は通る。 でも API 設計 はどうする? createServerFn 型が通ることと、境界が設計されていることは別です。 TSKaigi 2026 / TanStack Start の createServerFn で作る、型が通る API 3 / 13
createServerFn の使い方
import { z } from "zod";
export const createTodo = createServerFn({ method: "POST" })
.inputValidator(z.object({ title: z.string().min(1) }))
.handler(async ({ data }) => {
return await db.todo.create({ data });
});
コンポーネントなどから呼び出せる
// React
await createTodo({ data: { title: "LT
資料を書く" } });
server-only な処理を、クライアントから呼べる
引数は data として渡す
inputValidator で Runtime Validation できる
TSKaigi 2026 / TanStack Start の createServerFn で作る、型が通る API
4 / 13
何が壊れやすいのか handler が太る 境界がロジック置き場になる 単体テストしづらくなる 再利用しづらくなる 出力の境界が曖昧になる 返してよい field が不明確 内部用の値が混ざる クライアント向けの型として 不安定になる 責務が混ざる loader が DB 取得の 実装を持つ serverFn が業務ロジックの 本体になる コンポーネントが DB 由来の型に依存する 型が通ることと、境界が安定していることは別 TSKaigi 2026 / TanStack Start の createServerFn で作る、型が通る API 5 / 13
指針 1: serverFn は薄くする
NG: handler に全部書く
export const createTodo = createServerFn({
method: "POST",
}).handler(async ({ data, context }) => {
//
// validation
//
// DB
//
// ...
});
認可
ビジネスルール
操作
通知
OK: 境界に徹する
export const createTodo = createServerFn({
method: "POST"
}).inputValidator(createTodoInputSchema)
.handler(async ({ data }) => {
return todoOutputSchema.parse(
await createTodoUseCase(data),
);
});
serverFn は 境界、UseCase は 業務ロジック
UseCase を直接テストできる形にする
TSKaigi 2026 / TanStack Start の createServerFn で作る、型が通る API
6 / 13
指針 2: 出力も Schema 化する
const todoOutputSchema = z.object({
id: z.string(),
title: z.string(),
done: z.boolean(),
createdAt: z.number().int(),
});
export const createTodo = createServerFn({ method: "POST" })
.inputValidator(createTodoInputSchema)
.handler(async ({ data }) => {
const row = await createTodoUseCase(data);
return todoOutputSchema.parse(row);
});
推論に頼らず、出力を Schema 化する
型安全でも返すべきデータとは限らない
TSKaigi 2026 / TanStack Start の createServerFn で作る、型が通る API
7 / 13
指針 3 (1/2): loader は server-only ではない
TanStack Start の loader は isomorphic
SSR 時はサーバー、ページ遷移時はクライアントで動く
DB / secret は loader ではなく serverFn に閉じ込める
を
とみなして
に触っている
// NG: loader
server-only
DB
export const Route = createFileRoute("/todos")({
loader: async () => {
const session = readSessionCookie();
return await db.todo.findMany({
where: { userId: session.userId },
});
},
});
loader
は Route とデータ取得をつなぐ場所。DB や認可は serverFn に閉じ込める
TSKaigi 2026 / TanStack Start の createServerFn で作る、型が通る API
8 / 13
指針 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: "GET" }).handler(async () => {
return todosOutputSchema.parse(await listTodosUseCase());
});
export const Route = createFileRoute("/todos")({
loader: () => listTodos(),
});
serverFn
は Write 専用ではない。DB に触る Read も createServerFn を使う。
TSKaigi 2026 / TanStack Start の createServerFn で作る、型が通る API
9 / 13
ファイル分割例:責務で 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
コロケーション 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
補足:他の 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
まとめ の価値は エンドポイント定義を省略できること だけではない 本質は クライアントとサーバーの境界に型付きの契約を置くこと 契約を壊れにくくするために 1. serverFn は 薄く 保つ 2. 入力 と出力 を Schema 化する 3. loader と serverFn の責務を分ける createServerFn