API開発で最も非効率な作業は「仕様書とコードの二重メンテナンス」です。OpenAPIのYAMLを書いて、それに合わせてルーターを実装して、型定義を手書きして、テストも書く——この繰り返しに時間を取られていませんか?
スキーマファースト開発では、まずスキーマ(仕様)を定義し、そこからコード・型・テストを自動生成します。Claude Codeはこのワークフローを加速する最適なツールです。要件からOpenAPI YAMLを一発生成し、tRPCのルーターをZodスキーマ付きで自動構築し、スキーマ変更のbreaking changeをCIで自動検出する——この記事ではその具体的な方法を解説します。
スキーマファースト開発とは
スキーマファースト開発は、APIの仕様書(スキーマ)を最初に定義し、実装コードはスキーマから自動生成するアプローチです。2026年のTypeScript開発では2つのパターンが主流です。
| パターン | スキーマ | 生成物 | ユースケース |
|---|---|---|---|
| OpenAPIファースト | OpenAPI 3.1 YAML | 型定義・クライアント・バリデーション・モック | 公開API・複数言語クライアント・外部連携 |
| tRPC + Zodファースト | Zodスキーマ | 型推論・ルーター・クライアント(全自動) | フルスタックTypeScript・社内モノレポ |
CLAUDE.mdへのAPI設計ルール記述
## API設計ルール
### 原則
- スキーマファースト: OpenAPI 3.1 YAMLまたはZodスキーマを先に定義し、コードを生成する
- 手書きfetch禁止: openapi-fetchまたはtRPCクライアントを使う
- レスポンス形式: { data: T, error: null } | { data: null, error: { code, message } }
- 認証: Bearer token (Authorization header)
- バージョニング: URLパス /api/v1/
- エラーフォーマット: RFC 7807 Problem Details準拠
### OpenAPIルール
- 仕様ファイル: openapi/openapi.yaml
- 新規エンドポイント追加時は必ずopenapi.yamlを先に更新
- コード生成: openapi-typescript + openapi-fetchを使用
- バリデーション: npx @redocly/cli lint openapi/openapi.yaml
### tRPCルール
- Zodスキーマを先に定義してからルーターを実装
- inputバリデーションは必ずZodスキーマで定義
- Drizzle連携時はdrizzle-zodのcreateInsertSchema/createSelectSchemaを使う
- SuperjsonはhttpBatchLink内のtransformerに指定(v11記法)
### テスト
- OpenAPI: Orval + MSWでモック自動生成
- tRPC: supertest + callerによる直接テスト
- テストカバレッジ: エンドポイント100%
CLAUDE.mdの詳しい書き方はCLAUDE.md完全ガイドで解説しています。
OpenAPIスキーマファースト開発
要件からOpenAPI YAMLを自動生成する
以下の要件からOpenAPI 3.1仕様のYAMLを生成してください:
エンドポイント:
- GET /api/v1/users(一覧取得、cursorページネーション)
- POST /api/v1/users(新規作成)
- GET /api/v1/users/{id}(詳細取得)
- PUT /api/v1/users/{id}(更新)
- DELETE /api/v1/users/{id}(削除)
共通仕様:
- 認証: Bearer token
- エラー: RFC 7807 Problem Details
- レスポンス: { data: T } / { error: { type, title, status, detail } }
- cursorページネーション: ?cursor=xxx&limit=20
出力先: openapi/openapi.yaml
スキーマのcomponents/schemasも含めてください。
openapi: "3.1.0"
info:
title: User API
version: "1.0.0"
servers:
- url: /api/v1
paths:
/users:
get:
operationId: listUsers
summary: ユーザー一覧取得
parameters:
- name: cursor
in: query
schema: { type: string }
- name: limit
in: query
schema: { type: integer, default: 20, maximum: 100 }
responses:
"200":
description: 成功
content:
application/json:
schema:
type: object
properties:
data:
type: array
items: { $ref: "#/components/schemas/User" }
nextCursor:
type: string
nullable: true
security:
- BearerAuth: []
components:
schemas:
User:
type: object
required: [id, name, email, createdAt]
properties:
id: { type: string, format: uuid }
name: { type: string, minLength: 1, maxLength: 100 }
email: { type: string, format: email }
createdAt: { type: string, format: date-time }
securitySchemes:
BearerAuth:
type: http
scheme: bearer
OpenAPIから型定義・クライアントを自動生成する
生成したYAMLから、TypeScriptの型定義と型安全なHTTPクライアントを自動生成します。
# openapi-typescriptで型定義を生成 npx openapi-typescript openapi/openapi.yaml -o src/api/schema.d.ts # 生成された型を使ってopenapi-fetchでAPIクライアントを構築 npm install openapi-fetch
import createClient from "openapi-fetch";
import type { paths } from "./schema";
const api = createClient<paths>({ baseUrl: "/api/v1" });
// 完全に型安全(パス・パラメータ・レスポンスすべて型チェック)
const { data, error } = await api.GET("/users/{id}", {
params: { path: { id: "user-123" } },
});
// data は User | undefined として推論される
// 一覧取得(cursorページネーション)
const { data: users } = await api.GET("/users", {
params: { query: { cursor: "abc", limit: 20 } },
});
openapi-fetchはバンドルサイズ6KB・300K ops/sで、他のOpenAPIクライアント生成ツールの中で最も軽量かつ高速です。TanStack QueryやSWRとの統合も簡単にできます。tRPC v11 + Zodスキーマファースト開発
フルスタックTypeScriptプロジェクト(Next.js + TypeScriptバックエンド)では、tRPCを使うと型定義の手動管理が完全に不要になります。
Zod → tRPCルーター → クライアントの一気通貫生成
tRPC v11でユーザーCRUDルーターを実装してください。 要件: - Zodスキーマで入力バリデーション - Drizzle ORMでDB操作(drizzle-zodのcreateInsertSchemaを使用) - cursorページネーション - エラーハンドリング(TRPCError) - v11記法(createTRPCClient、transformerはlink内) テーブル定義(db/schema.ts)はすでにあります。
import { z } from "zod";
import { router, publicProcedure } from "../trpc";
import { users } from "@/db/schema";
import { createInsertSchema } from "drizzle-orm/zod";
import { eq, gt, asc } from "drizzle-orm";
import { TRPCError } from "@trpc/server";
// DBスキーマからZodスキーマを自動生成
const createUserInput = createInsertSchema(users).omit({ id: true, createdAt: true });
const updateUserInput = createInsertSchema(users).partial().omit({ id: true, createdAt: true });
export const userRouter = router({
list: publicProcedure
.input(z.object({
cursor: z.string().optional(),
limit: z.number().min(1).max(100).default(20),
}))
.query(async ({ input, ctx }) => {
const items = await ctx.db
.select()
.from(users)
.where(input.cursor ? gt(users.id, Number(input.cursor)) : undefined)
.orderBy(asc(users.id))
.limit(input.limit + 1);
const hasMore = items.length > input.limit;
const data = hasMore ? items.slice(0, -1) : items;
return {
data,
nextCursor: hasMore ? String(data.at(-1)?.id) : null,
};
}),
getById: publicProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input, ctx }) => {
const [user] = await ctx.db
.select()
.from(users)
.where(eq(users.id, input.id));
if (!user) throw new TRPCError({ code: "NOT_FOUND", message: "ユーザーが見つかりません" });
return user;
}),
create: publicProcedure
.input(createUserInput)
.mutation(async ({ input, ctx }) => {
const [user] = await ctx.db.insert(users).values(input).returning();
return user;
}),
update: publicProcedure
.input(z.object({ id: z.number(), data: updateUserInput }))
.mutation(async ({ input, ctx }) => {
const [user] = await ctx.db
.update(users)
.set(input.data)
.where(eq(users.id, input.id))
.returning();
if (!user) throw new TRPCError({ code: "NOT_FOUND" });
return user;
}),
delete: publicProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input, ctx }) => {
await ctx.db.delete(users).where(eq(users.id, input.id));
return { success: true };
}),
});
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import superjson from "superjson";
import type { AppRouter } from "@/server/router";
const trpc = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: "/api/trpc",
transformer: superjson, // v11: transformerはlink内に指定
}),
],
});
// 完全に型安全(引数・戻り値すべて型推論される)
const user = await trpc.user.create.mutate({
name: "田中太郎",
email: "tanaka@example.com",
});
// user は { id: number, name: string, email: string, ... } として推論
createTRPCProxyClientがcreateTRPCClientに変更され、.interop()モード(v9互換)は完全廃止されました。またtransformerの指定場所がクライアント初期化からlink内に移動しています。CLAUDE.mdに明記しておきましょう。tRPCの基本的な使い方はTypeScript × tRPC完全ガイドをご覧ください。
REST(OpenAPI)vs tRPC ── 使い分けの判断基準
| 判断基準 | REST + OpenAPI | tRPC |
|---|---|---|
| クライアントの言語 | 複数言語(Swift/Kotlin/Python等) | TypeScriptのみ |
| APIの公開範囲 | 外部公開・サードパーティ連携 | 社内・モノレポ内 |
| リポジトリ構成 | サーバー・クライアント分離 | モノレポまたは単一リポジトリ |
| 型安全性 | コード生成で担保 | 型推論で自動担保 |
| ドキュメント | OpenAPI仕様書が自動生成 | なし(TypeScript型が仕様書代わり) |
| ランタイムバリデーション | 別途実装が必要 | Zodで自動 |
スキーマからテスト・モックを自動生成する
OpenAPI → MSWモック自動生成(Orval)
Orvalを使うと、OpenAPI仕様書からMSW(Mock Service Worker)のハンドラーとFaker.jsの生成データを一括で自動生成できます。バックエンドが未完成でもフロントエンドのテストが書けます。
petstore:
input: ./openapi/openapi.yaml
output:
target: ./src/api/generated
client: react-query # TanStack Queryフック自動生成
mock: true # MSWハンドラー自動生成
# コード生成(型・クライアント・MSWモック一括) npx orval # 生成されるファイル: # src/api/generated/ # ├── users.ts # TanStack Queryフック # ├── users.msw.ts # MSWハンドラー # └── model/ # 型定義
import { setupServer } from "msw/node";
import { getUsersHandler, getUsersByIdHandler } from "@/api/generated/users.msw";
import { render, screen } from "@testing-library/react";
import { UserList } from "./UserList";
// OpenAPIから自動生成されたMSWハンドラーを使用
const server = setupServer(...getUsersHandler(), ...getUsersByIdHandler());
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test("ユーザー一覧が表示される", async () => {
render(<UserList />);
// MSWがOpenAPIスキーマに基づくモックデータを返す
expect(await screen.findByText(/ユーザー/)).toBeTruthy();
});
tRPC → callerによるダイレクトテスト
import { appRouter } from "@/server/router";
import { createCallerFactory } from "@trpc/server";
const createCaller = createCallerFactory(appRouter);
describe("userRouter", () => {
it("ユーザーを作成して取得できる", async () => {
const caller = createCaller({ db: testDb });
// 作成
const created = await caller.user.create({
name: "テストユーザー",
email: "test@example.com",
});
expect(created.id).toBeDefined();
// 取得
const user = await caller.user.getById({ id: created.id });
expect(user.name).toBe("テストユーザー");
});
it("存在しないユーザーでNOT_FOUNDエラー", async () => {
const caller = createCaller({ db: testDb });
await expect(
caller.user.getById({ id: 999999 })
).rejects.toThrow("NOT_FOUND");
});
});
テスト全般の戦略はClaude Codeテスト完全ガイドで解説しています。
CIでAPIスキーマのbreaking changeを自動検出する
OpenAPI仕様書が変更されたとき、後方互換性を壊す変更(breaking change)をCIで自動検出できます。
name: API Schema Check
on:
pull_request:
paths:
- "openapi/**"
jobs:
breaking-changes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# OpenAPIバリデーション
- name: Lint OpenAPI
run: npx @redocly/cli lint openapi/openapi.yaml
# Breaking Change検出(oasdiff)
- uses: oasdiff/oasdiff-action/breaking@v0.0.37
with:
base: "main:openapi/openapi.yaml" # mainブランチの仕様
revision: "openapi/openapi.yaml" # PRの仕様
fail-on: ERR # breaking changeでCI失敗
oasdiffは300以上のbreaking changeルールを持ち、以下のような変更を自動検出します:
- 必須フィールドの追加
- フィールドの型変更(string→number等)
- エンドポイントの削除
- レスポンスコードの削除
- 認証要件の変更
GitHub Actionsの設定方法はClaude Code GitHub Actions完全ガイドをご覧ください。
よくある質問
@trpc/openapi(alpha)で、tRPCルーターからOpenAPI仕様書をCLI生成できます。またoRPC(tRPC互換フレームワーク)はOpenAPI生成を組み込みでサポートしており、新規プロジェクトでの有力な選択肢です。createTRPCProxyClientではなくcreateTRPCClient。transformerはlink内に指定。.interop()モードは使用しない。TypeScript 5.7.2+・Node.js 18+必須」@redocly/cli lintでバリデーション、oasdiffでbreaking change検出を実行するのが推奨パターンです。Claude Codeにスキーマ変更を依頼した直後にnpx @redocly/cli lint openapi/openapi.yamlを実行させる習慣もつけましょう。drizzle-zodのcreateInsertSchemaはZodスキーマを返すため、tRPCの.input()にそのまま渡せます。DB定義→Zodスキーマ→tRPC inputバリデーションの一気通貫が実現します。.omit()や.partial()でcreate/update用にカスタマイズもできます。まとめ
スキーマファースト開発をClaude Codeで実践するためのポイントをまとめます。
- OpenAPIファースト: 要件→OpenAPI YAML→openapi-typescript+openapi-fetchで型安全クライアント自動生成
- tRPC + Zodファースト: Zodスキーマ→tRPC v11ルーター→クライアント型推論で手動型定義ゼロ
- DBスキーマ逆生成: drizzle-zodでDBスキーマ→Zodスキーマ→tRPC inputバリデーションを一気通貫
- テスト自動生成: Orval + MSWでOpenAPIからモック・テストヘルパーを自動生成
- breaking change検出: oasdiff + GitHub ActionsでPRごとにスキーマ互換性を自動チェック
- 使い分け: 公開API→REST + OpenAPI、内部BFF→tRPC。「二刀流」が2026年のスタンダード
データベース連携はClaude Code × データベース開発ガイド、Next.jsとの統合はClaude Code × Next.js完全ガイドもあわせてご覧ください。

