API デザイン パターン
一貫性のある開発者フレンドリーな REST API を設計するための規約とベスト プラクティス。
アクティブ化するとき
- 新しい API エンドポイントを設計しているとき
- 既存の API 契約をレビューしているとき
- ページネーション、フィルタリング、またはソートを追加しているとき
- API のエラー処理を実装しているとき
- API バージョン管理戦略を計画しているとき
- パブリックまたはパートナー向けの API を構築しているとき
リソース デザイン
URL 構造
# リソースは名詞、複数形、小文字、ケバブケース GET /api/v1/users GET /api/v1/users/:id POST /api/v1/users PUT /api/v1/users/:id PATCH /api/v1/users/:id DELETE /api/v1/users/:id # 関係のための サブ リソース GET /api/v1/users/:id/orders POST /api/v1/users/:id/orders # CRUD にマップされないアクション (動詞は慎重に使用) POST /api/v1/orders/:id/cancel POST /api/v1/auth/login POST /api/v1/auth/refresh
命名規則
# よい /api/v1/team-members # 複数単語リソース用ケバブケース /api/v1/orders?status=active # フィルタリング用クエリ パラメーター /api/v1/users/123/orders # 所有権用のネストされたリソース # 悪い /api/v1/getUsers # URL 内の動詞 /api/v1/user # 単数形(複数形を使用) /api/v1/team_members # URL 内のスネークケース /api/v1/users/123/getOrders # ネストされたリソース内の動詞
HTTP メソッドとステータス コード
メソッド セマンティクス
| メソッド | べき等 | セーフ | 使用対象 |
|---|---|---|---|
| GET | はい | はい | リソースを取得 |
| POST | いいえ | いいえ | リソースを作成、アクションをトリガー |
| PUT | はい | いいえ | リソースの完全な置換 |
| PATCH | いいえ* | いいえ | リソースの部分的な更新 |
| DELETE | はい | いいえ | リソースを削除 |
*PATCH は適切な実装でべき等にすることができます
ステータス コード リファレンス
# 成功 200 OK — GET、PUT、PATCH(応答本体付き) 201 Created — POST (Location ヘッダーを含める) 204 No Content — DELETE、PUT(応答本体なし) # クライアント エラー 400 Bad Request — 検証失敗、不正な JSON 401 Unauthorized — 認証がない、または無効 403 Forbidden — 認証済みですが認可されていない 404 Not Found — リソースが存在しません 409 Conflict — 重複エントリ、状態競合 422 Unprocessable Entity — セマンティック上無効(有効な JSON、悪いデータ) 429 Too Many Requests — レート制限を超過 # サーバー エラー 500 Internal Server Error — 予期しない失敗 (詳細は公開しない) 502 Bad Gateway — アップストリーム サービスが失敗 503 Service Unavailable — 一時的なオーバーロード、Retry-After を含める
一般的な間違い
# 悪い: すべてに 200 { "status": 200, "success": false, "error": "Not found" } # よい: HTTP ステータス コードをセマンティック的に使用 HTTP/1.1 404 Not Found { "error": { "code": "not_found", "message": "User not found" } } # 悪い: 検証エラーに 500 # よい: フィールドレベルの詳細を含む 400 または 422 # 悪い: 作成されたリソースに 200 # よい: Location ヘッダー付き 201 HTTP/1.1 201 Created Location: /api/v1/users/abc-123
応答フォーマット
成功応答
json{ "data": { "id": "abc-123", "email": "alice@example.com", "name": "Alice", "created_at": "2025-01-15T10:30:00Z" } }
コレクション応答(ページネーション付き)
json{ "data": [ { "id": "abc-123", "name": "Alice" }, { "id": "def-456", "name": "Bob" } ], "meta": { "total": 142, "page": 1, "per_page": 20, "total_pages": 8 }, "links": { "self": "/api/v1/users?page=1&per_page=20", "next": "/api/v1/users?page=2&per_page=20", "last": "/api/v1/users?page=8&per_page=20" } }
エラー応答
json{ "error": { "code": "validation_error", "message": "Request validation failed", "details": [ { "field": "email", "message": "Must be a valid email address", "code": "invalid_format" }, { "field": "age", "message": "Must be between 0 and 150", "code": "out_of_range" } ] } }
応答エンベロープ バリエーション
typescript// オプション A: データ ラッパー付きエンベロープ(パブリック API に推奨) interface ApiResponse<T> { data: T; meta?: PaginationMeta; links?: PaginationLinks; } interface ApiError { error: { code: string; message: string; details?: FieldError[]; }; } // オプション B: フラット応答(シンプル、内部 API 向け) // 成功: リソースを直接返す // エラー: エラー オブジェクトを返す // HTTP ステータス コードで区別
ページネーション
オフセット ベース(シンプル)
GET /api/v1/users?page=2&per_page=20 # 実装 SELECT * FROM users ORDER BY created_at DESC LIMIT 20 OFFSET 20;
長所: 実装が簡単、「N ページにジャンプ」をサポート 短所: 大きなオフセット(OFFSET 100000)で低速、同時挿入で矛盾
カーソル ベース(スケーラブル)
GET /api/v1/users?cursor=eyJpZCI6MTIzfQ&limit=20 # 実装 SELECT * FROM users WHERE id > :cursor_id ORDER BY id ASC LIMIT 21; -- 次が있는지 判定するため 1 つ余分に取得
json{ "data": [...], "meta": { "has_next": true, "next_cursor": "eyJpZCI6MTQzfQ" } }
長所: 位置に関わらず一貫性のあるパフォーマンス、同時挿入では安定 短所: 任意のページへのジャンプができない、カーソルが不透明
どちらを使用するか
| ユースケース | ページネーション タイプ |
|---|---|
| 管理ダッシュボード、小さなデータセット(<10K) | オフセット |
| 無限スクロール、フィード、大きなデータセット | カーソル |
| パブリック API | カーソル(デフォルト)とオフセット(オプション) |
| 検索結果 | オフセット(ユーザーはページ番号を期待) |
フィルタリング、ソート、検索
フィルタリング
# シンプルな等価性 GET /api/v1/orders?status=active&customer_id=abc-123 # 比較演算子(括弧表記を使用) GET /api/v1/products?price[gte]=10&price[lte]=100 GET /api/v1/orders?created_at[after]=2025-01-01 # 複数値(カンマ区切り) GET /api/v1/products?category=electronics,clothing # ネストされたフィールド(ドット表記) GET /api/v1/orders?customer.country=US
ソート
# 単一フィールド (降順用に - を頭に付ける) GET /api/v1/products?sort=-created_at # 複数フィールド(カンマ区切り) GET /api/v1/products?sort=-featured,price,-created_at
全文検索
# 検索クエリ パラメーター GET /api/v1/products?q=wireless+headphones # フィールド固有の検索 GET /api/v1/users?email=alice
スパース フィールドセット
# 指定されたフィールドのみを返す(ペイロード削減) GET /api/v1/users?fields=id,name,email GET /api/v1/orders?fields=id,total,status&include=customer.name
認証と認可
トークン ベース認証
# Authorization ヘッダー内のベアラー トークン GET /api/v1/users Authorization: Bearer eyJhbGciOiJIUzI1NiIs... # API キー(サーバー間) GET /api/v1/data X-API-Key: sk_live_abc123
認可パターン
typescript// リソース レベル: 所有権を確認 app.get("/api/v1/orders/:id", async (req, res) => { const order = await Order.findById(req.params.id); if (!order) return res.status(404).json({ error: { code: "not_found" } }); if (order.userId !== req.user.id) return res.status(403).json({ error: { code: "forbidden" } }); return res.json({ data: order }); }); // ロール ベース: 権限を確認 app.delete("/api/v1/users/:id", requireRole("admin"), async (req, res) => { await User.delete(req.params.id); return res.status(204).send(); });
レート制限
ヘッダー
HTTP/1.1 200 OK X-RateLimit-Limit: 100 X-RateLimit-Remaining: 95 X-RateLimit-Reset: 1640000000 # 超過した場合 HTTP/1.1 429 Too Many Requests Retry-After: 60 { "error": { "code": "rate_limit_exceeded", "message": "Rate limit exceeded. Try again in 60 seconds." } }
レート制限ティア
| ティア | 制限 | ウィンドウ | ユースケース |
|---|---|---|---|
| 匿名 | 30/分 | IP あたり | パブリック エンドポイント |
| 認証済み | 100/分 | ユーザーあたり | 標準 API アクセス |
| プレミアム | 1000/分 | API キーあたり | 有料 API プラン |
| 内部 | 10000/分 | サービスあたり | サービス間通信 |
バージョン管理
URL パス バージョン管理(推奨)
/api/v1/users /api/v2/users
長所: 明示的、ルーティングが簡単、キャッシャブル 短所: バージョン間で URL が変更される
ヘッダー バージョン管理
GET /api/users Accept: application/vnd.myapp.v2+json
長所: クリーンな URL 短所: テストが困難、忘れやすい
バージョン管理戦略
1. /api/v1/ から開始 — 必要になるまでバージョン管理しないでください 2. 最大 2 つのアクティブ バージョンを保守(現在 + 前) 3. 廃止予定のタイムライン: - 廃止予定を発表(パブリック API には 6 か月前の通知) - Sunset ヘッダーを追加: Sunset: Sat, 01 Jan 2026 00:00:00 GMT - 廃止予定日後に 410 Gone を返す 4. 非破壊的な変更はバージョン新規が必要ありません: - 応答への新しいフィールドの追加 - 新しいオプション クエリ パラメーターの追加 - 新しいエンドポイントの追加 5. 破壊的な変更には新しいバージョンが必要です: - フィールドの削除または名前変更 - フィールド型の変更 - URL 構造の変更 - 認証方法の変更
実装パターン
TypeScript (Next.js API ルート)
typescriptimport { z } from "zod"; import { NextRequest, NextResponse } from "next/server"; const createUserSchema = z.object({ email: z.string().email(), name: z.string().min(1).max(100), }); export async function POST(req: NextRequest) { const body = await req.json(); const parsed = createUserSchema.safeParse(body); if (!parsed.success) { return NextResponse.json({ error: { code: "validation_error", message: "Request validation failed", details: parsed.error.issues.map(i => ({ field: i.path.join("."), message: i.message, code: i.code, })), }, }, { status: 422 }); } const user = await createUser(parsed.data); return NextResponse.json( { data: user }, { status: 201, headers: { Location: `/api/v1/users/${user.id}` }, }, ); }
API デザイン チェックリスト
新しいエンドポイントを本番環境に配信する前に:
- リソース URL は命名規則に従う(複数形、ケバブケース、動詞なし)
- 正しい HTTP メソッドが使用されている(読み取り用 GET、作成用 POST など)
- 適切なステータス コードが返される(すべてに 200 ではない)
- 入力がスキーマで検証される(Zod、Pydantic、Bean Validation)
- エラー応答は標準フォーマットに従う(コードとメッセージ付き)
- ページネーションはリスト エンドポイントに実装される(カーソルまたはオフセット)
- 認証が必要(または明示的にパブリックとしてマーク)
- 認可が確認される(ユーザーは自分のリソースにのみアクセス可能)
- レート制限が設定される
- 応答は内部詳細をリークしない(スタック トレース、SQL エラー)
- 既存のエンドポイントと命名が一貫している(camelCase vs snake_case)
- ドキュメント化される(OpenAPI/Swagger スペック更新)

