Skip to content
Published on

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

Authors

はじめに

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回送っても結果(サーバー状態)が同一であることです。

メソッド冪等安全説明
GETOOリソース状態変更なし
HEADOOGETと同一、本文なし
OPTIONSOOメタデータのみ返却
PUTOX同じデータで何度上書きしても結果同一
DELETEOX既に削除済みリソースの削除試行は404だが状態同一
POSTXX毎回新しいリソースを作成
PATCHXX相対的な変更の場合、結果が異なる可能性

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 成功

コード名前使用タイミング
200OKGET成功、PUT/PATCH成功時
201CreatedPOSTでリソース作成時(Locationヘッダー含む)
202Accepted非同期操作受付(後で処理)
204No ContentDELETE成功、PUT成功(本文なし)

3xx リダイレクション

コード名前使用タイミング
301Moved Permanentlyリソース永久移動(キャッシュされる)
302Found一時リダイレクト
304Not Modifiedキャッシュ有効(ETag/Last-Modified)

4xx クライアントエラー

コード名前使用タイミング
400Bad Request不正なリクエスト形式、バリデーション失敗
401Unauthorized認証必要(トークンなし/期限切れ)
403Forbidden認証済みだが権限なし
404Not Foundリソースなし
405Method Not Allowedそのリソースで許可されないメソッド
409Conflictリソース競合(重複作成等)
422Unprocessable Entity構文は正しいが意味的エラー
429Too Many Requestsレート制限超過

5xx サーバーエラー

コード名前使用タイミング
500Internal Server Error予期せぬサーバーエラー
502Bad Gatewayアップストリームサーバーエラー
503Service Unavailableサービス一時利用不可(メンテナンス等)
504Gateway 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;

ページネーション比較表

特性OffsetCursorKeyset
実装難易度簡単普通難しい
特定ページへの移動OXX
大規模パフォーマンス悪い良い良い
データ変更時の一貫性悪い良い良い
複合ソート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、GoogleMicrosoft、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ティア設計

ティアリクエスト/分リクエスト/日同時接続数
Free601,0005
Basic30010,00020
Pro1,000100,00050
Enterprise10,0001,000,000200

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&currency=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は開発者体験を向上させ、システム間の統合を容易にし、ビジネス価値を創出します。

キーポイントの振り返り:

  1. リソース中心のURL設計 — 名詞、複数形、最大2階層ネスト
  2. 正しいHTTPメソッド — セマンティクスと冪等性の理解
  3. 正確なステータスコード — クライアントがエラーを正しく処理できるように
  4. Cursorページネーション — 大規模データに必須
  5. RFC 9457エラー形式 — 一貫した有用なエラーレスポンス
  6. URLパスバージョニング — 最も実用的な選択
  7. Rate Limiting — API安定性と公平な使用の保証
  8. OpenAPI 3.1 — 仕様自動化とコード生成

これらの原則を適用して、開発者に愛されるAPIを作りましょう。