- Published on
REST API設計ベストプラクティス2025:開発者が知るべきAPI設計原則と実践パターン
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- はじめに
- 1. API設計が重要な理由
- 2. REST基礎:Richardson Maturity Model
- 3. URL設計:命名規則とパターン
- 4. HTTPメソッド深堀り
- 5. ステータスコード戦略
- 6. ページネーションパターン
- 7. フィルタリング、ソート、フィールド選択
- 8. バージョニング戦略
- 9. エラーハンドリング:RFC 9457 Problem Details
- 10. Rate Limitingとスロットリング
- 11. セキュリティ
- 12. OpenAPI 3.1
- 13. APIテスト
- 14. 実践事例:代表的なAPI設計パターン
- 15. クイズ
- 参考資料
- まとめ
はじめに
API経済の規模は2027年までに13.7兆ドルに達すると予測されています。Stripe、Twilio、SendGridのようなAPIファーストの企業が数十億ドルの価値を生み出しており、全ての現代ソフトウェアはAPIを通じて接続されています。優れた設計のAPIは開発者体験(DX)を最大化し、メンテナンスを容易にし、ビジネス成長の核心的な推進力となります。
この記事では、REST API設計の基礎から高度なパターンまで、2025年現在の業界標準と実践で検証されたベストプラクティスを体系的に解説します。Richardson Maturity Model、URL設計、HTTPメソッド、ステータスコード、ページネーション、エラーハンドリング、バージョニング、セキュリティ、OpenAPIまで全てのトピックをコード例と共に説明します。
1. API設計が重要な理由
API経済の成長
| 指標 | 数値 |
|---|---|
| グローバルAPI経済規模(2027年予測) | 13.7兆ドル |
| Salesforce売上のAPI比率 | 50%以上 |
| 平均企業が使用するAPI数 | 15,000+ |
| APIファースト企業の成長率 | 従来企業の2.6倍 |
悪いAPI設計のコスト
- 開発者オンボーディング時間の増加(平均2週間追加)
- 顧客離脱率の増加(悪いDXは30%高い離脱率)
- メンテナンスコストの指数的増加
- Breaking changeによるクライアント障害
APIファースト設計原則
APIファーストとは、アプリケーション実装前にAPI仕様を先に設計するアプローチです。
# ステップ1:OpenAPI仕様を先に作成
openapi: 3.1.0
info:
title: User Service API
version: 1.0.0
paths:
/api/v1/users:
get:
summary: List users
parameters:
- name: page
in: query
schema:
type: integer
responses:
'200':
description: Success
設計ワークフロー:
1. API仕様作成(OpenAPI)
2. 仕様レビューと合意
3. モックサーバーでフロントエンド開発開始
4. バックエンド実装
5. コントラクトテスト
2. REST基礎:Richardson Maturity Model
Leonard Richardsonが提案したREST API成熟度モデルは4段階に分かれます。
Level 0:The Swamp of POX
全てのリクエストが一つのエンドポイントに送信されます。SOAPスタイルと類似しています。
POST /api HTTP/1.1
Content-Type: application/json
{
"action": "getUser",
"userId": 123
}
Level 1:Resources
URIを通じて個別リソースを識別しますが、HTTPメソッドを適切に活用しません。
POST /api/users/123 HTTP/1.1
Content-Type: application/json
{
"action": "get"
}
Level 2:HTTP Verbs
HTTPメソッドを正しく使用してリソースに対する操作を表現します。ほとんどのREST APIがこのレベルです。
GET /api/users/123 HTTP/1.1
Accept: application/json
---
PUT /api/users/123 HTTP/1.1
Content-Type: application/json
{
"name": "Updated Name",
"email": "user@example.com"
}
Level 3:Hypermedia Controls(HATEOAS)
レスポンスに関連リソースリンクを含め、クライアントが動的にAPIを探索できるようにします。
{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"_links": {
"self": { "href": "/api/v1/users/123" },
"orders": { "href": "/api/v1/users/123/orders" },
"update": { "href": "/api/v1/users/123", "method": "PUT" },
"delete": { "href": "/api/v1/users/123", "method": "DELETE" }
}
}
RESTの6つの制約条件
| 制約条件 | 説明 |
|---|---|
| Client-Server | クライアントとサーバーの関心事の分離 |
| Stateless | 各リクエストは独立、サーバーにセッション保存しない |
| Cacheable | レスポンスにキャッシュ可能性を明示 |
| Uniform Interface | 一貫したインターフェースでの相互作用 |
| Layered System | 中間層(プロキシ、ロードバランサー)を許容 |
| Code on Demand(任意) | サーバーがクライアントに実行可能コードを送信可能 |
3. URL設計:命名規則とパターン
核心原則
- 名詞を使用(動詞ではない)— リソースを表現
- 複数形を使用 —
/users、/orders、/products - 小文字とハイフン —
/user-profiles(snake_caseやcamelCaseではない) - 階層構造 —
/users/123/orders/456 - 動詞は例外的な場合のみ —
/users/123/activate(状態変更アクション)
URL設計20例
# コレクション
GET /api/v1/users # ユーザー一覧
POST /api/v1/users # ユーザー作成
# 個別リソース
GET /api/v1/users/123 # 特定ユーザー取得
PUT /api/v1/users/123 # ユーザー全体更新
PATCH /api/v1/users/123 # ユーザー部分更新
DELETE /api/v1/users/123 # ユーザー削除
# ネストリソース
GET /api/v1/users/123/orders # ユーザーの注文一覧
GET /api/v1/users/123/orders/456 # 特定注文取得
POST /api/v1/users/123/orders # 注文作成
# フィルタリング
GET /api/v1/users?role=admin # 管理者のみ取得
GET /api/v1/products?category=electronics&min_price=100
# 検索
GET /api/v1/users/search?q=john # ユーザー検索
# 状態変更(アクション)
POST /api/v1/users/123/activate # アカウント有効化
POST /api/v1/orders/456/cancel # 注文キャンセル
POST /api/v1/payments/789/refund # 決済返金
# バッチ操作
POST /api/v1/users/batch # 一括作成
DELETE /api/v1/users/batch # 一括削除
# サブリソース(シングルトン)
GET /api/v1/users/123/profile # ユーザープロフィール
PUT /api/v1/users/123/avatar # アバター更新
ネスト vs フラット — いつ何を使うか?
| パターン | 例 | 長所 | 短所 |
|---|---|---|---|
| ネスト | /users/123/orders | 所有関係が明確 | 深いネストで複雑化 |
| フラット | /orders?user_id=123 | 独立的アクセス可能 | 関係が不明確 |
| ハイブリッド | 2階層までネスト、以降クエリ | バランスの取れたアプローチ | - |
実践ガイド:ネストは最大2階層までにしましょう。
/users/123/ordersはOKですが、/users/123/orders/456/items/789/reviewsは深すぎます。
4. HTTPメソッド深堀り
メソッドのセマンティクス
GET リソース取得(読み取り) — 安全、冪等
POST リソース作成(書き込み) — 安全でない、非冪等
PUT リソース全体置換(上書き) — 安全でない、冪等
PATCH リソース部分更新 — 安全でない、非冪等(一般的に)
DELETE リソース削除 — 安全でない、冪等
OPTIONS サポートメソッド確認 — 安全、冪等
HEAD GETと同一、本文なし — 安全、冪等
冪等性(Idempotency)の深い理解
冪等性とは、同じリクエストを1回送っても100回送っても結果(サーバー状態)が同一であることです。
| メソッド | 冪等 | 安全 | 説明 |
|---|---|---|---|
| GET | O | O | リソース状態変更なし |
| HEAD | O | O | GETと同一、本文なし |
| OPTIONS | O | O | メタデータのみ返却 |
| PUT | O | X | 同じデータで何度上書きしても結果同一 |
| DELETE | O | X | 既に削除済みリソースの削除試行は404だが状態同一 |
| POST | X | X | 毎回新しいリソースを作成 |
| PATCH | X | X | 相対的な変更の場合、結果が異なる可能性 |
PUT vs PATCHの違い
// PUT:リソース全体置換(全フィールド必要)
// PUT /api/v1/users/123
{
"name": "John Doe",
"email": "john@example.com",
"age": 30,
"role": "admin"
}
// PATCH:部分更新(変更フィールドのみ)
// PATCH /api/v1/users/123
{
"age": 31
}
作成時のPOST vs PUT
POST /api/v1/users -> サーバーがID生成(201 Created + Locationヘッダー)
PUT /api/v1/users/123 -> クライアントがID指定(存在すれば置換、なければ作成)
5. ステータスコード戦略
2xx 成功
| コード | 名前 | 使用タイミング |
|---|---|---|
| 200 | OK | GET成功、PUT/PATCH成功時 |
| 201 | Created | POSTでリソース作成時(Locationヘッダー含む) |
| 202 | Accepted | 非同期操作受付(後で処理) |
| 204 | No Content | DELETE成功、PUT成功(本文なし) |
3xx リダイレクション
| コード | 名前 | 使用タイミング |
|---|---|---|
| 301 | Moved Permanently | リソース永久移動(キャッシュされる) |
| 302 | Found | 一時リダイレクト |
| 304 | Not Modified | キャッシュ有効(ETag/Last-Modified) |
4xx クライアントエラー
| コード | 名前 | 使用タイミング |
|---|---|---|
| 400 | Bad Request | 不正なリクエスト形式、バリデーション失敗 |
| 401 | Unauthorized | 認証必要(トークンなし/期限切れ) |
| 403 | Forbidden | 認証済みだが権限なし |
| 404 | Not Found | リソースなし |
| 405 | Method Not Allowed | そのリソースで許可されないメソッド |
| 409 | Conflict | リソース競合(重複作成等) |
| 422 | Unprocessable Entity | 構文は正しいが意味的エラー |
| 429 | Too Many Requests | レート制限超過 |
5xx サーバーエラー
| コード | 名前 | 使用タイミング |
|---|---|---|
| 500 | Internal Server Error | 予期せぬサーバーエラー |
| 502 | Bad Gateway | アップストリームサーバーエラー |
| 503 | Service Unavailable | サービス一時利用不可(メンテナンス等) |
| 504 | Gateway Timeout | アップストリームサーバータイムアウト |
ステータスコード選択フロー
リクエスト成功?
├── YES -> データ返却? -> 200 OK
│ リソース作成? -> 201 Created
│ 非同期操作? -> 202 Accepted
│ 本文なし? -> 204 No Content
└── NO -> 認証問題? -> 401 / 403
リクエスト問題? -> 400 / 422
リソースなし? -> 404
競合? -> 409
サーバー問題? -> 500 / 502 / 503
6. ページネーションパターン
Offsetベースページネーション
最も簡単ですが、大規模データでパフォーマンス問題があります。
GET /api/v1/users?page=3&size=20
{
"data": [
{ "id": 41, "name": "User 41" },
{ "id": 42, "name": "User 42" }
],
"pagination": {
"page": 3,
"size": 20,
"total_items": 1000,
"total_pages": 50,
"has_next": true,
"has_prev": true
}
}
-- SQL実装
SELECT * FROM users
ORDER BY id
LIMIT 20 OFFSET 40;
-- 問題:OFFSET 10000だと10000件をスキップ → パフォーマンス低下
Cursorベースページネーション
大規模データに最適です。カーソル(最後の項目のID)を使用します。
GET /api/v1/users?cursor=eyJpZCI6NDJ9&limit=20
{
"data": [
{ "id": 43, "name": "User 43" },
{ "id": 44, "name": "User 44" }
],
"pagination": {
"next_cursor": "eyJpZCI6NjJ9",
"has_next": true,
"limit": 20
}
}
// Node.js実装
async function getCursorPaginatedUsers(cursor, limit = 20) {
let query = 'SELECT * FROM users'
const params = []
if (cursor) {
const decoded = JSON.parse(Buffer.from(cursor, 'base64').toString())
query += ' WHERE id > $1'
params.push(decoded.id)
}
query += ' ORDER BY id ASC LIMIT $' + (params.length + 1)
params.push(limit + 1) // +1で次ページ存在確認
const rows = await db.query(query, params)
const hasNext = rows.length > limit
const data = hasNext ? rows.slice(0, limit) : rows
const nextCursor = hasNext
? Buffer.from(JSON.stringify({ id: data[data.length - 1].id })).toString('base64')
: null
return { data, pagination: { next_cursor: nextCursor, has_next: hasNext, limit } }
}
Keysetベースページネーション
複合ソートに最適です。
GET /api/v1/users?after_date=2025-01-15&after_id=42&limit=20&sort=created_at
-- SQL実装:created_at + id複合キー
SELECT * FROM users
WHERE (created_at, id) > ('2025-01-15', 42)
ORDER BY created_at ASC, id ASC
LIMIT 20;
ページネーション比較表
| 特性 | Offset | Cursor | Keyset |
|---|---|---|---|
| 実装難易度 | 簡単 | 普通 | 難しい |
| 特定ページへの移動 | O | X | X |
| 大規模パフォーマンス | 悪い | 良い | 良い |
| データ変更時の一貫性 | 悪い | 良い | 良い |
| 複合ソート | O | 制限的 | O |
| 総数提供 | O | コスト大 | コスト大 |
| 使用例 | 管理画面 | SNSフィード | 時系列データ |
7. フィルタリング、ソート、フィールド選択
フィルタリングパターン
# 単純フィルタ
GET /api/v1/products?category=electronics&brand=apple
# 比較演算子
GET /api/v1/products?price_gte=100&price_lte=500
# 配列フィルタ
GET /api/v1/products?tags=wireless,bluetooth
# 日付範囲
GET /api/v1/orders?created_after=2025-01-01&created_before=2025-03-31
ソート
# 単一ソート
GET /api/v1/products?sort=price
# 降順
GET /api/v1/products?sort=-price
# 複合ソート
GET /api/v1/products?sort=-created_at,name
フィールド選択(Sparse Fieldsets)
# 必要なフィールドのみリクエスト(レスポンスサイズ削減)
GET /api/v1/users?fields=id,name,email
# 関連リソースを含める
GET /api/v1/users/123?include=orders,profile
{
"id": 123,
"name": "John Doe",
"email": "john@example.com"
}
高度なフィルタリング:RSQL/FIQLスタイル
# RQL(Resource Query Language)スタイル
GET /api/v1/products?filter=category==electronics;price=gt=100;brand=in=(apple,samsung)
8. バージョニング戦略
3つのアプローチ
URLパスバージョニング(最も一般的)
GET /api/v1/users/123
GET /api/v2/users/123
ヘッダーバージョニング
GET /api/users/123
Accept: application/vnd.myapp.v2+json
クエリパラメータバージョニング
GET /api/users/123?version=2
比較表
| 特性 | URLパス | ヘッダー | クエリパラメータ |
|---|---|---|---|
| 可視性 | 非常に高い | 低い | 普通 |
| キャッシング | 簡単 | 複雑 | 普通 |
| 実装難易度 | 簡単 | 普通 | 簡単 |
| ブラウザテスト | 簡単 | 難しい | 簡単 |
| APIルーティング | 明確 | 追加ロジック必要 | 追加ロジック必要 |
| 使用企業 | GitHub、Stripe、Google | Microsoft、GitHub(GraphQL) | AWS、Netflix |
バージョニングベストプラクティス
1. メジャーバージョンのみ管理(v1、v2)— Minor/Patchは後方互換
2. 最低2バージョン同時サポート
3. 非推奨ポリシー公開(最低6ヶ月の猶予期間)
4. Sunsetヘッダー使用
Sunset: Sat, 01 Mar 2026 00:00:00 GMT
Deprecation: true
Link: <https://api.example.com/v2/users>; rel="successor-version"
9. エラーハンドリング:RFC 9457 Problem Details
RFC 9457標準エラー形式
2023年にRFC 7807を置き換えたRFC 9457は、HTTP APIの標準エラーレスポンス形式を定義しています。
{
"type": "https://api.example.com/errors/validation",
"title": "Validation Failed",
"status": 422,
"detail": "リクエスト本文に無効なフィールドがあります。",
"instance": "/api/v1/users",
"timestamp": "2025-03-15T10:30:00Z",
"errors": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "有効なメールアドレスを入力してください。"
},
{
"field": "age",
"code": "OUT_OF_RANGE",
"message": "年齢は0から150の間でなければなりません。"
}
]
}
エラーレスポンス実装(Node.js/Express)
// エラークラス定義
class ApiError extends Error {
constructor(status, type, title, detail, errors = []) {
super(detail)
this.status = status
this.type = type
this.title = title
this.detail = detail
this.errors = errors
}
}
// ファクトリ関数
const ApiErrors = {
notFound: (resource, id) =>
new ApiError(
404,
'https://api.example.com/errors/not-found',
'Resource Not Found',
`${resource} with id ${id} was not found.`
),
validationFailed: (errors) =>
new ApiError(
422,
'https://api.example.com/errors/validation',
'Validation Failed',
'The request body contains invalid fields.',
errors
),
rateLimited: (retryAfter) =>
new ApiError(
429,
'https://api.example.com/errors/rate-limit',
'Rate Limit Exceeded',
`Too many requests. Retry after ${retryAfter} seconds.`
),
}
// Expressエラーハンドラー
app.use((err, req, res, next) => {
if (err instanceof ApiError) {
return res.status(err.status).json({
type: err.type,
title: err.title,
status: err.status,
detail: err.detail,
instance: req.originalUrl,
timestamp: new Date().toISOString(),
errors: err.errors.length > 0 ? err.errors : undefined,
})
}
// 予期せぬエラー
console.error('Unexpected error:', err)
res.status(500).json({
type: 'https://api.example.com/errors/internal',
title: 'Internal Server Error',
status: 500,
detail: 'An unexpected error occurred.',
instance: req.originalUrl,
timestamp: new Date().toISOString(),
})
})
多言語エラーメッセージ
// i18nエラーメッセージ
const errorMessages = {
INVALID_EMAIL: {
ko: '유효한 이메일 주소를 입력하세요.',
en: 'Please enter a valid email address.',
ja: '有効なメールアドレスを入力してください。',
},
FIELD_REQUIRED: {
ko: '필수 입력 항목입니다.',
en: 'This field is required.',
ja: '必須入力項目です。',
},
}
function getErrorMessage(code, lang = 'en') {
return errorMessages[code]?.[lang] || errorMessages[code]?.['en'] || code
}
10. Rate Limitingとスロットリング
アルゴリズム比較
Token Bucket
バケット容量:100トークン
補充速度:10トークン/秒
[リクエスト到着] → バケットにトークンあり? → YES → トークン消費、リクエスト処理
→ NO → 429レスポンス
class TokenBucket {
constructor(capacity, refillRate) {
this.capacity = capacity
this.tokens = capacity
this.refillRate = refillRate
this.lastRefill = Date.now()
}
consume() {
this.refill()
if (this.tokens > 0) {
this.tokens--
return true
}
return false
}
refill() {
const now = Date.now()
const elapsed = (now - this.lastRefill) / 1000
this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillRate)
this.lastRefill = now
}
}
Sliding Window Log
class SlidingWindowLog {
constructor(windowMs, maxRequests) {
this.windowMs = windowMs
this.maxRequests = maxRequests
this.logs = new Map() // key -> timestamps[]
}
isAllowed(key) {
const now = Date.now()
const windowStart = now - this.windowMs
if (!this.logs.has(key)) {
this.logs.set(key, [])
}
const timestamps = this.logs.get(key).filter((ts) => ts > windowStart)
this.logs.set(key, timestamps)
if (timestamps.length < this.maxRequests) {
timestamps.push(now)
return true
}
return false
}
}
Rate Limitレスポンスヘッダー
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 998
X-RateLimit-Reset: 1678886400
---
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1678886400
Retry-After: 30
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/rate-limit",
"title": "Rate Limit Exceeded",
"status": 429,
"detail": "You have exceeded the rate limit. Please retry after 30 seconds."
}
Rate Limitティア設計
| ティア | リクエスト/分 | リクエスト/日 | 同時接続数 |
|---|---|---|---|
| Free | 60 | 1,000 | 5 |
| Basic | 300 | 10,000 | 20 |
| Pro | 1,000 | 100,000 | 50 |
| Enterprise | 10,000 | 1,000,000 | 200 |
11. セキュリティ
OAuth2フロー要約
Authorization Code Flow(サーバーアプリ):
1. ユーザー → 認証サーバー(ログイン)
2. 認証サーバー → クライアント(Authorization Code)
3. クライアント → 認証サーバー(Code + Client Secret → Access Token)
4. クライアント → リソースサーバー(Access Token)
Authorization Code + PKCE(SPA/モバイル):
1. code_verifier生成
2. code_challenge = SHA256(code_verifier)
3. 認証リクエストにcode_challengeを含める
4. トークン交換時にcode_verifierを送信して検証
JWTベストプラクティス
1. Access Token有効期限:15分(最大1時間)
2. Refresh Token有効期限:7日(最大30日)
3. 署名アルゴリズム:RS256(非対称)またはES256推奨
4. ペイロードに機密情報を入れないこと
5. aud、iss、expクレームを必ず検証
6. httpOnlyクッキーに保存(XSS防止)
7. Token Rotation適用
APIキー管理
# ヘッダー方式(推奨)
GET /api/v1/users HTTP/1.1
Authorization: Bearer access_token_here
X-API-Key: api_key_here
# URLに含めないこと
# 悪い例: /api/v1/users?api_key=abc123
CORS設定
// Express CORS設定
const cors = require('cors')
app.use(
cors({
origin: ['https://app.example.com', 'https://admin.example.com'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
credentials: true,
maxAge: 86400, // Preflightキャッシュ24時間
})
)
入力バリデーション
// Zodを使用したリクエスト検証
const { z } = require('zod')
const CreateUserSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
age: z.number().int().min(0).max(150).optional(),
role: z.enum(['user', 'admin', 'moderator']).default('user'),
})
// ミドルウェア
function validate(schema) {
return (req, res, next) => {
const result = schema.safeParse(req.body)
if (!result.success) {
return res.status(422).json({
type: 'https://api.example.com/errors/validation',
title: 'Validation Failed',
status: 422,
errors: result.error.issues.map((issue) => ({
field: issue.path.join('.'),
code: issue.code,
message: issue.message,
})),
})
}
req.body = result.data
next()
}
}
app.post('/api/v1/users', validate(CreateUserSchema), createUser)
12. OpenAPI 3.1
スペック構造
openapi: 3.1.0
info:
title: E-Commerce API
description: E-Commerce REST API
version: 1.0.0
contact:
name: API Team
email: api@example.com
servers:
- url: https://api.example.com/v1
description: Production
- url: https://staging-api.example.com/v1
description: Staging
paths:
/products:
get:
operationId: listProducts
tags: [Products]
summary: List all products
parameters:
- name: category
in: query
schema:
type: string
- name: sort
in: query
schema:
type: string
enum: [price, -price, name, -name, created_at]
- name: cursor
in: query
schema:
type: string
- name: limit
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Product'
pagination:
$ref: '#/components/schemas/CursorPagination'
'429':
$ref: '#/components/responses/RateLimited'
components:
schemas:
Product:
type: object
required: [id, name, price]
properties:
id:
type: integer
format: int64
name:
type: string
minLength: 1
maxLength: 200
price:
type: number
format: decimal
minimum: 0
category:
type: string
created_at:
type: string
format: date-time
CursorPagination:
type: object
properties:
next_cursor:
type: string
nullable: true
has_next:
type: boolean
limit:
type: integer
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
security:
- BearerAuth: []
- ApiKeyAuth: []
Contract-First vs Code-First
| アプローチ | 長所 | 短所 |
|---|---|---|
| Contract-First | 仕様ベースの合意、モックサーバー即座生成、コード生成 | 仕様維持管理の追加コスト |
| Code-First | 高速開発、コードが真実の源 | 仕様とコードの不一致の可能性 |
コード生成
# OpenAPI GeneratorでクライアントSDK生成
npx @openapitools/openapi-generator-cli generate \
-i openapi.yaml \
-g typescript-axios \
-o ./generated/client
# サーバースタブ生成
npx @openapitools/openapi-generator-cli generate \
-i openapi.yaml \
-g nodejs-express-server \
-o ./generated/server
13. APIテスト
Postman/Newmanで自動化テスト
// Postman Test Script例
pm.test('Status code is 200', () => {
pm.response.to.have.status(200)
})
pm.test('Response has correct structure', () => {
const json = pm.response.json()
pm.expect(json).to.have.property('data')
pm.expect(json.data).to.be.an('array')
pm.expect(json).to.have.property('pagination')
})
pm.test('Pagination cursor exists', () => {
const json = pm.response.json()
if (json.data.length > 0) {
pm.expect(json.pagination).to.have.property('next_cursor')
}
})
# NewmanでCI/CDパイプラインで実行
newman run collection.json \
--environment production.json \
--reporters cli,junit \
--reporter-junit-export results.xml
PactによるContract Testing
// Consumer Test(フロントエンド)
const { PactV3 } = require('@pact-foundation/pact')
const provider = new PactV3({
consumer: 'WebApp',
provider: 'UserService',
})
describe('User API', () => {
it('fetches a user by ID', async () => {
await provider
.given('user 123 exists')
.uponReceiving('a request for user 123')
.withRequest({
method: 'GET',
path: '/api/v1/users/123',
headers: { Accept: 'application/json' },
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
id: 123,
name: 'John Doe',
email: 'john@example.com',
},
})
.executeTest(async (mockServer) => {
const response = await fetch(`${mockServer.url}/api/v1/users/123`, {
headers: { Accept: 'application/json' },
})
expect(response.status).toBe(200)
})
})
})
k6負荷テスト
// k6ロードテスト
import http from 'k6/http'
import { check, sleep } from 'k6'
export const options = {
stages: [
{ duration: '30s', target: 50 }, // ランプアップ
{ duration: '1m', target: 100 }, // 維持
{ duration: '30s', target: 0 }, // ランプダウン
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95%のリクエストが500ms以内
http_req_failed: ['rate<0.01'], // エラー率1%未満
},
}
export default function () {
const res = http.get('https://api.example.com/v1/products?limit=20')
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
'has data array': (r) => JSON.parse(r.body).data.length > 0,
})
sleep(1)
}
14. 実践事例:代表的なAPI設計パターン
Stripe API
Stripeは業界最高のAPI DXを提供しています。
Stripeのコア設計原則:
1. 一貫したリソース命名(/v1/customers、/v1/charges、/v1/subscriptions)
2. Expandパラメータで関連リソースを含む
3. 冪等性キー(Idempotency-Keyヘッダー)
4. 詳細なエラーレスポンス
5. 自動ページネーション(has_more + starting_after)
# Stripeスタイルリクエスト
POST /v1/charges HTTP/1.1
Authorization: Bearer sk_test_xxx
Idempotency-Key: unique-request-id-123
Content-Type: application/x-www-form-urlencoded
amount=2000¤cy=usd&source=tok_visa
GitHub API
GitHub APIの特徴:
1. REST + GraphQLハイブリッド
2. 条件付きリクエスト(ETag、If-None-Match)
3. Linkヘッダーページネーション
4. Hypermedia(URLテンプレート)
5. Rate Limitの明確な表示
# GitHub条件付きリクエスト
GET /repos/octocat/hello-world HTTP/1.1
If-None-Match: "etag-value"
# 304 Not Modified(キャッシュ使用)
# または 200 OK(新しいETag含む)
Twitter(X) API v2
Twitter API v2の特徴:
1. フィールド選択(fieldsパラメータ)
2. Expansionで関連オブジェクトを含む
3. ストリーミングエンドポイント
4. 細分化されたRate Limit
5. OAuth2 + App-only Auth
# Twitterスタイル:フィールド選択 + expansion
GET /2/tweets?ids=123,456&tweet.fields=created_at,public_metrics&expansions=author_id&user.fields=username
15. クイズ
Q1. PUTとPATCHの主な違いは何ですか?
PUTはリソース全体を置換(全フィールド必要)し、PATCHはリソースの一部のフィールドのみを変更します。PUTは冪等性が保証されますが、PATCHは相対的な変更(例:カウンター増加)の場合、冪等性が保証されないことがあります。
Q2. Richardson Maturity Model Level 3は何を意味しますか?
Level 3はHATEOAS(Hypermedia as the Engine of Application State)を意味します。APIレスポンスに関連リソースへのリンクを含め、クライアントがハードコードされたURLなしにAPIを動的に探索できるようにします。真のRESTの完成段階です。
Q3. Offsetページネーションの問題点と代替手段は?
OffsetページネーションはOFFSETが大きくなるほどデータベースがスキップする行が多くなり、パフォーマンスが低下します。また、データの追加・削除時に重複や欠落が発生する可能性があります。代替手段としてCursorベースページネーションがあり、最後の項目のIDをカーソルとして使用してWHERE id条件で効率的にクエリします。
Q4. HTTP 401と403の違いは?
401 Unauthorizedは認証が必要であることを意味します(トークンなしまたは期限切れ)。403 Forbiddenは認証済みだが該当リソースに対する権限がないことを意味します。つまり、401は「どなたですか?」であり、403は「あなたは分かっていますが、ここにはアクセスできません」です。
Q5. APIバージョニングでURLパス方式の長所と短所は?
長所:可視性が高く、キャッシングが簡単で、ブラウザで直接テスト可能で、ルーティングが明確です。短所:URLが長くなり、バージョン変更時に全クライアントがURLを更新する必要があり、同じリソースに対して複数のURLが存在することになります。
Q6. RFC 9457 Problem Detailsの主要フィールドは?
RFC 9457のコアフィールドはtype(エラー種別URI)、title(人間が読めるエラー要約)、status(HTTPステータスコード)、detail(具体的な説明)、instance(問題が発生したURI)です。typeとtitleは事実上必須であり、その他は任意ですが含めることが推奨されます。
Q7. Token BucketとSliding Windowアルゴリズムの違いは?
Token Bucketはバケットにトークンが一定速度で補充され、リクエスト時にトークンを消費する方式です。バーストトラフィックを許容します。Sliding Windowは時間ウィンドウ内のリクエスト数を追跡し、ウィンドウが移動しながら古いリクエストを削除します。より精密ですがメモリをより多く使用します。
参考資料
- RFC 9110:HTTP Semantics
- RFC 9457:Problem Details for HTTP APIs
- RFC 6749:OAuth 2.0 Authorization Framework
- OpenAPI Specification 3.1.0
- Roy FieldingのREST論文(2000年)
- Stripe API Reference
- GitHub REST API Documentation
- Google API Design Guide
- Microsoft REST API Guidelines
- JSON:API Specification
まとめ
REST API設計は技術的な決定以上の意味を持ちます。優れたAPIは開発者体験を向上させ、システム間の統合を容易にし、ビジネス価値を創出します。
キーポイントの振り返り:
- リソース中心のURL設計 — 名詞、複数形、最大2階層ネスト
- 正しいHTTPメソッド — セマンティクスと冪等性の理解
- 正確なステータスコード — クライアントがエラーを正しく処理できるように
- Cursorページネーション — 大規模データに必須
- RFC 9457エラー形式 — 一貫した有用なエラーレスポンス
- URLパスバージョニング — 最も実用的な選択
- Rate Limiting — API安定性と公平な使用の保証
- OpenAPI 3.1 — 仕様自動化とコード生成
これらの原則を適用して、開発者に愛されるAPIを作りましょう。