- Published on
API設計完全ガイド — REST・OpenAPI・Versioning・Pagination・Idempotency・Webhooks 2025年版
- Authors

- Name
- Youngju Kim
- @fjvbn20031
プロローグ — 「良いAPIは愛される」
2026年4月、Stripeのドキュメントを開く。リクエスト・レスポンス・SDKサンプルが1ページに並び、エラーは一貫したコードと丁寧なメッセージ、バージョンは明確(Stripe-Version: 2024-12-18)、Rate limit・Idempotency・Webhook署名も標準化されている。
なぜStripeが「最高のAPI」と呼ばれるのか。一貫性、予測可能性、ドキュメント — これは運ではなく設計の結果だ。
パフォーマンスが技術的品質なら、API設計は開発者というユーザーへのUXである。良いAPIは予測可能で、安定し、寛容で、制限・非推奨化を前もって知らせる透明性を持つ。
1部 — Richardson Maturity Model
Level 0: 単一エンドポイント + POST ですべて
Level 1: リソースごとのURL(まだverbが残る)
Level 2: HTTPメソッドを正しく使う
Level 3: HATEOAS(ハイパーメディア)
現実: ほとんどのAPIはLevel 2。HATEOASは理論的には優雅だが実装コストに見合わない。
2部 — REST API設計原則10
1. リソース指向URL
Good: /users/123/orders
Bad: /getUserOrders?userId=123
2. 複数形
Good: /users, /orders, /products
Bad: /user, /order(混在)
3. HTTPメソッドの意味を守る
| Method | 意味 | Idempotent |
|---|---|---|
| GET | 読み取り(safe) | Yes |
| POST | 作成 | No |
| PUT | 全置換 | Yes |
| PATCH | 部分更新 | 設計次第 |
| DELETE | 削除 | Yes |
4. Status Codeを正しく
200 OK / 201 Created / 202 Accepted / 204 No Content
400 Bad Request / 401 Unauthorized / 403 Forbidden
404 / 409 Conflict / 422 Unprocessable / 429 Too Many Requests
500 / 502 / 503 / 504
よくあるアンチパターン: すべてのエラーを200で返しbodyに{ "error": ... }を入れる。絶対禁止。
5. RFC 9457準拠のエラー形式
{
"type": "https://example.com/errors/insufficient-funds",
"title": "Insufficient funds",
"status": 422,
"detail": "Account balance $50 is less than required $100",
"instance": "/accounts/12345/transactions/abc",
"errors": [
{ "field": "amount", "message": "must be <= balance" }
],
"requestId": "req_abc123"
}
RFC 9457(Problem Details)が2023年にRFC 7807を置き換えた。
6. レスポンス形状の一貫性
エンベロープ形式かbare objectか、どちらかに統一する。
7. 時刻はISO 8601 + UTC
{ "createdAt": "2026-04-15T12:34:56Z" }
8. 金額は整数(minor unit)
{ "amount": 10000, "currency": "JPY" }
floatは精度問題を生む。
9. Boolean命名
isActive, hasPermission, canEdit など意味が明確な名前に。
10. IDは接頭辞付き文字列
{ "id": "usr_01HX9G7..." }
接頭辞(usr_, ord_)はデバッグを助け、連番整数の公開は列挙攻撃を招く。
3部 — OpenAPI 3.1実践
基本構造
openapi: 3.1.0
info:
title: Example API
version: 1.0.0
servers:
- url: https://api.example.com/v1
paths:
/users:
get:
summary: List users
operationId: listUsers
parameters:
- name: limit
in: query
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
- name: cursor
in: query
schema: { type: string }
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/UserList'
components:
schemas:
User:
type: object
required: [id, email]
properties:
id: { type: string, pattern: '^usr_' }
email: { type: string, format: email }
createdAt: { type: string, format: date-time }
ZodをSingle Source of Truthに
import { z } from 'zod';
import { generateSchema } from '@anatine/zod-openapi';
export const User = z.object({
id: z.string().startsWith('usr_'),
email: z.string().email(),
createdAt: z.string().datetime(),
});
export type User = z.infer<typeof User>;
const openApiSchema = generateSchema(User);
2025年の主要ツール
| ツール | 用途 |
|---|---|
| Zod + @anatine/zod-openapi | TS-first、スキーマ自動生成 |
| Typebox | 型フレンドリーなスキーマ |
| Hono + zod-openapi | ルーター + OpenAPI自動 |
| tRPC + trpc-openapi | tRPCからOpenAPI変換 |
| FastAPI (Python) | Pydantic + OpenAPI標準 |
| Spectral | Linting |
| Scalar, Mintlify | Docs UI |
Spec-first vs Code-first: 2025年はCode-first優勢。Specは派生成果物。
4部 — Versioning戦略
5つの方式
- URL path —
/v1/users,/v2/users。明快、キャッシュに優しい。 - Accept header —
Accept: application/vnd.example.v1+json。URLはきれいだがデバッグが難しい。 - Custom header —
X-API-Version: 2024-12-18。Stripe方式。 - Query parameter —
?api-version=2。RESTらしくない。 - Content negotiation —
Accept: application/json; version=2。
2025年の推奨
公開API: URL path (v1, v2) + 日付ヘッダー
内部API: Headerベース、rolling update
モバイル: URL path、古いアプリも長期サポート
Stripeの日付バージョニング
すべての版を永久にサポートし、内部でversion translatorが橋渡しをする。教訓は、バージョニングは長期コミットメントということ。
Breaking vs Non-breaking
Non-breaking: フィールド・エンドポイント・オプションパラメータ・enum値の追加(クライアントがunknown値を処理できる前提)。
Breaking: フィールドの削除・改名、型変更、required追加、URL構造変更、エラーコード変更。
5部 — Pagination
1. Offset-based
GET /users?limit=20&offset=40
シンプルでUIに優しい。Deep offsetは遅く、同時insertで重複・抜けが発生。
2. Cursor-based
GET /users?limit=20&cursor=eyJpZCI6MTAwfQ==
{ "data": [...], "nextCursor": "eyJpZCI6MTIwfQ==", "hasMore": true }
Deep pagination高速、順序一貫。ランダムアクセス不可。
3. Keyset(Seek Method)
-- 遅いoffset
SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 100000;
-- 高速なkeyset
SELECT * FROM users WHERE id > 100020 ORDER BY id LIMIT 20;
非常に高速、インデックスを活かす。ソート条件変更時に複雑化。
選択指針
「1 2 3 ... 10」UI → Offset
無限スクロール・API → Cursor
内部バッチ、大テーブル → Keyset
6部 — Idempotency
ユーザーが「支払」ボタンを押してタイムアウト、クライアントが再試行 — サーバーは同じ要求を2回受け取り二重課金になる。解決策がIdempotency Key。
Stripe方式
POST /charges HTTP/1.1
Idempotency-Key: f8a9b2c3-...-xyz
Content-Type: application/json
{ "amount": 10000, "currency": "JPY" }
サーバーロジック:
1. Idempotency-Keyをチェック
2. 存在する場合:
- 同じbody → 保存済みレスポンス返却
- 異なるbody → 409 Conflict
3. 存在しない場合: 新規処理 + 結果を保存(24時間TTL)
対象メソッド
- GET/PUT/DELETE — 本来Idempotent
- POST — 必須
- PATCH — 設計次第
Node.js + Redis実装例
async function idempotentHandler(req, res) {
const key = req.headers['idempotency-key'];
if (!key) return res.status(400).json({ error: 'Idempotency-Key required' });
const lock = await redis.set(`lock:${key}`, '1', 'NX', 'EX', 30);
if (!lock) await sleep(500);
const cached = await redis.get(`idempotency:${key}`);
if (cached) {
const { bodyHash, response } = JSON.parse(cached);
if (bodyHash !== hash(req.body)) {
return res.status(409).json({ error: 'Conflicting request' });
}
return res.status(response.status).json(response.body);
}
const result = await processPayment(req.body);
await redis.set(
`idempotency:${key}`,
JSON.stringify({ bodyHash: hash(req.body), response: { status: 200, body: result } }),
'EX', 86400
);
return res.json(result);
}
7部 — Rate Limiting
3つのアルゴリズム
- Token Bucket — 一定速度でトークン補充、バケットサイズまでバースト許容。実装容易。
- Leaky Bucket — キューが一定速度で流れ、バーストを平滑化。
- Sliding Window — 直近N秒を正確にカウント(Redis sorted setで実装)。正確だが難しい。
レスポンスヘッダー
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1697050800
# または RFC 9466 (draft):
RateLimit-Limit: 100, 100;w=60
RateLimit-Remaining: 42
RateLimit-Reset: 58
429 + Retry-After
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
{ "error": "Rate limit exceeded", "retryAfter": 60 }
Redis+Lua、Cloudflare Rate Limiting、Envoy Rate Limit、Kong/Tyk/Traefikなどで実装。
8部 — Webhook設計
7つの必須要素
- Signing — HMAC-SHA256を
timestamp + "." + bodyに計算しX-Example-Signatureで送信。送信元の確認と改竄防止。
function verifyWebhook(body: string, signature: string, secret: string): boolean {
const [t, v1] = signature.split(',').map(s => s.split('=')[1]);
const timestamp = parseInt(t);
if (Math.abs(Date.now()/1000 - timestamp) > 300) return false;
const expected = hmacSha256(secret, `${t}.${body}`);
return timingSafeEqual(expected, v1);
}
- Timestamp — 5分以上前は拒否しreplay攻撃を防ぐ。
- Retry with Backoff — 1分 → 5分 → 15分 → 1時間 → 6時間 → 24時間、最大3日。
- Idempotency — Webhook IDで受信側が重複処理を防ぐ。
- Ordered vs Unordered — Stripeはunordered、受信側がAPIで最新状態を再確認。
- Version —
Accept-VersionヘッダーまたはペイロードのapiVersion。 - Testing/Replay — ダッシュボードから再送、Stripe CLI
stripe listen、開発環境でも署名検証を無効化しない。
受信側チェックリスト
1. 署名検証を最初に
2. Timestamp確認(5分以内)
3. Event IDで重複排除
4. 200を即返却、重い処理はキューへ
5. 非同期処理(Bull, Celeryなど)
6. 失敗時は5xxで発信側に再送させる
9部 — AsyncAPIとイベント駆動
CloudEvents(CNCF標準)
{
"specversion": "1.0",
"type": "com.example.order.created",
"source": "/orders",
"id": "evt_abc",
"time": "2026-04-15T12:00:00Z",
"datacontenttype": "application/json",
"data": { "orderId": 123, "amount": 10000 }
}
AsyncAPI 3.0
asyncapi: 3.0.0
info:
title: Order Service Events
version: 1.0.0
channels:
orderCreated:
address: orders.created
messages:
orderCreatedMessage:
payload:
type: object
properties:
orderId: { type: string }
amount: { type: integer }
Webhook vs Queue vs Stream
| 方式 | 特徴 | 例 |
|---|---|---|
| Webhook | HTTP POST push | Stripe, GitHub |
| Queue | Pull, 1:1 | SQS, RabbitMQ |
| Stream | Pull, 1:N, 順序保持 | Kafka, Kinesis |
外部統合 → Webhook、内部ジョブ → Queue、監査・分析 → Stream。
10部 — REST vs GraphQL vs gRPC vs tRPC
REST + OpenAPI: 公開API、キャッシュ、多様なクライアント
GraphQL: 複雑なデータグラフ、フィールド選択
gRPC: 内部マイクロサービス、高性能、多言語
tRPC: TSモノレポフルスタック、型自動
TypeSpec(Microsoft)が台頭 — OpenAPIへコンパイルされる簡潔なIDL。
11部 — セキュリティと認証
| 方式 | 特徴 |
|---|---|
| API Key | 簡単、ローテーション困難 |
| OAuth 2.0 | ユーザー委任、複雑 |
| JWT | 無状態、失効困難 |
| Session Cookie | 失効容易、CSRF注意 |
| mTLS | サービス間、強力 |
API Keyベストプラクティス
- 接頭辞(
sk_live_,pk_test_)で種別明示 - 32文字以上ランダム
- ハッシュ保存、比較はハッシュ対ハッシュ
- 権限分離(read-only、admin)
- ローテーション容易に(旧+新の重複期間)
- Secret Scanner(GitHub)連携
CORS
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization, Idempotency-Key
Access-Control-Max-Age: 86400
Allow-Origin: * + credentials はブラウザが拒否。必ず明示的なoriginを。
12部 — DeprecationとSunset
Sunsetヘッダー(RFC 8594)
Sunset: Wed, 01 Jan 2027 00:00:00 GMT
Deprecation: true
Link: <https://docs.example.com/v2>; rel="successor-version"
ポリシー: 6ヶ月前からDeprecationヘッダー、3ヶ月前にメール通知、1ヶ月前に最終通知、sunset後は410 Gone。移行ガイド、使用量ダッシュボード、SDKからの警告も用意する。
13部 — 6ヶ月ロードマップ
- 1ヶ月目: REST原則10、メソッド・ステータス・エラー形式
- 2ヶ月目: OpenAPI 3.1 + Zod + zod-openapi
- 3ヶ月目: Pagination 3方式、Cursorエンコード/デコード
- 4ヶ月目: Idempotency + Rate LimitingをRedisで
- 5ヶ月目: Webhookシステム — 署名、再送、ダッシュボード
- 6ヶ月目: ドキュメント自動化(Scalar, Mintlify)、SDK生成
14部 — 12項目チェックリスト
- OpenAPI 3.1の自動生成(code-first)
- HTTPメソッド・ステータス一貫
- エラー形式RFC 9457準拠
- Idempotency-KeyをPOSTで必須化
- Rate-limitヘッダー
- 新規エンドポイントはCursor pagination
- Webhook署名(HMAC-SHA256)
- Webhook再送 + ダッシュボード
- Versioningポリシー文書化
- Sunset/Deprecationヘッダー
- CORSは明示的origin
- SDK自動生成
15部 — アンチパターン10
- すべてのエラーを200で返す
- URLにverb(
/getUserById) - Paginationなしのリスト
- Idempotencyなしの決済API
- エラー形式が不一致
- バージョン変更なしのbreaking change
- Webhook署名検証スキップ
- Rate Limitなしの公開API
- 連番整数IDの公開
- 通知なしのエンドポイント削除
まとめ — 「APIは契約である」
APIは一度公開すると変更が難しく、顧客コードが依存し、breaking changeはコストになる。だからこそ慎重に設計し、徹底的に文書化し、長く維持する。
2025年の十戒:
- リソース指向・複数形URL
- HTTPメソッド・ステータスを正確に
- エラーはRFC 9457形式
- OpenAPI 3.1をSingle Source of Truthに
- 明確なバージョニングポリシー
- Cursor-based Pagination
- POSTにIdempotency-Key
- Rate Limit + 標準ヘッダー
- Webhook署名 + 再送 + ダッシュボード
- Deprecation通知 + Sunsetヘッダー
次回はSaaSアーキテクチャ完全ガイド — Multi-tenancy、Billing、Feature Flag、Audit Log、RBAC/ABAC、Usage Metering。APIを作ったなら、次はそれをSaaSとして売る方法だ。