Skip to content
Published on

REST API設計ベストプラクティス2025:ネーミング、バージョニング、エラー処理、ページネーション、セキュリティ

Authors

はじめに

REST APIは現代のWebサービスの基盤です。モバイルアプリ、SPA、マイクロサービス、IoTデバイスなど、ほぼ全てのソフトウェアがREST APIを通じて通信しています。しかし「RESTful」と主張しながら、実際にはREST原則に違反しているAPIが驚くほど多いのが現状です。

適切に設計されたAPIは開発者体験(DX)を向上させ、メンテナンスを容易にし、スケーラビリティとセキュリティを保証します。逆に、設計が不十分なAPIはチーム間のコミュニケーションコストを増加させ、セキュリティの脆弱性を生み出し、技術的負債を蓄積させます。

このガイドでは、リソースネーミングからバージョニング、エラー処理、認証、OpenAPIまで、REST API設計のあらゆるベストプラクティスを網羅します。


1. REST原則(Principles)

1.1 RESTの6つの制約条件

1. Client-Server(クライアント-サーバー分離)
   - UIとデータ保存の関心事を分離
   - 独立した進化が可能

2. Stateless(ステートレス)
   - 各リクエストは完全な情報を含む
   - サーバーはクライアントの状態を保存しない
   - スケーラビリティの向上

3. Cacheable(キャッシュ可能)
   - レスポンスはキャッシュ可能かを宣言する必要がある
   - Cache-Control, ETag, Last-Modifiedヘッダーの活用

4. Uniform Interface(統一インターフェース)
   - リソースの識別(URI   - 表現を通じたリソース操作
   - 自己記述的メッセージ
   - HATEOAS

5. Layered System(階層型システム)
   - クライアントは直接サーバーと通信しているか中間者かを区別できない
   - ロードバランサー、キャッシュ、ゲートウェイの追加が可能

6. Code-on-Demand(オプション)
   - サーバーがクライアントに実行可能コードを送信可能
   - 唯一のオプション制約

1.2 Richardsonの成熟度モデル

Level 3: ハイパーメディアコントロール(HATEOAS  --> レスポンスに関連リソースへのリンクを含む
Level 2: HTTPメソッド
  --> GET, POST, PUT, DELETEの正しい使用
Level 1: リソース
  --> リソースごとの個別URI
Level 0: POXの沼
  --> 単一エンドポイント、全操作をPOSTで実行

ほとんどのAPIはLevel 2を目標とする。
Level 3HATEOAS)は理想だが実務ではオプション。

2. リソースネーミング

2.1 基本ルール

Good:
GET    /users               # ユーザー一覧
GET    /users/123            # 特定ユーザー取得
POST   /users               # ユーザー作成
PUT    /users/123            # ユーザー全体更新
PATCH  /users/123            # ユーザー部分更新
DELETE /users/123            # ユーザー削除

Bad:
GET    /getUsers             # 動詞は使わない
GET    /user/123             # 複数形を使用
POST   /createUser           # 動詞は使わない
POST   /user/123/delete      # HTTPメソッドを使用

2.2 階層的関係

# ユーザーの注文一覧
GET /users/123/orders

# ユーザーの特定注文
GET /users/123/orders/456

# 注文の商品一覧
GET /users/123/orders/456/items

# 注意:3階層以上のネストは避ける
# Bad:
GET /users/123/orders/456/items/789/reviews

# Good(代替案):
GET /items/789/reviews
GET /reviews?item_id=789

2.3 ネーミングコンベンション

# 小文字 + ハイフン(kebab-case)を使用
Good: /user-profiles
Bad:  /userProfiles, /user_profiles, /UserProfiles

# ファイル拡張子なし
Good: Accept: application/json
Bad:  /users/123.json

# 末尾スラッシュなし
Good: /users
Bad:  /users/

# フィルタリングにはクエリパラメータを使用
GET /users?status=active&role=admin
GET /products?category=electronics&min_price=100&max_price=500

# ソート
GET /users?sort=created_at&order=desc
GET /users?sort=-created_at,+name  # - 降順, + 昇順

# フィールド選択(Sparse Fieldsets)
GET /users/123?fields=name,email,avatar

2.4 非リソース操作の処理

# アクション:サブリソースとして表現
POST /users/123/activate          # ユーザー有効化
POST /users/123/deactivate        # ユーザー無効化
POST /orders/456/cancel           # 注文キャンセル
POST /payments/789/refund         # 返金処理

# 検索(動詞的アクション、リソースではない)
GET /search?q=keyword&type=users

# バッチ操作
POST /users/batch
Body: { "ids": [1, 2, 3], "action": "deactivate" }

# 集計
GET /orders/statistics
GET /dashboard/metrics

3. HTTPメソッドの正しい使用

3.1 メソッドの特性

メソッド目的冪等性安全リクエストボディレスポンスボディ
GET読み取りはいはいなしあり
POST作成いいえいいえありあり
PUT全体置換はいいいえありオプション
PATCH部分更新いいえ*いいえありあり
DELETE削除はいいいえオプションオプション
HEADヘッダーのみはいはいなしなし
OPTIONS利用可能メソッドはいはいなしあり

*PATCHは実装によって冪等にできる

3.2 冪等性の詳細

# PUT - 冪等(同じリクエストを複数回送っても同じ結果)
PUT /users/123
Body: { "name": "Alice", "email": "alice@example.com" }
# 1回送っても10回送っても結果は同じ

# DELETE - 冪等
DELETE /users/123
# 1回目: 200 OK(削除完了)
# 2回目: 404 Not Found(既に削除済み) - 副作用なし

# POST - 冪等ではない(各リクエストが新しいリソースを作成)
POST /orders
Body: { "product_id": 1, "quantity": 2 }
# 2回送ると2件の注文が作成される!

冪等性キーでPOSTを安全にする:

POST /payments
Headers:
  Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Body: { "amount": 10000, "currency": "USD" }

# 同じIdempotency-Keyで再送しても重複決済を防止
# サーバーがキーを保存し、同じキーには以前のレスポンスを返す

3.3 PUT vs PATCH

# 元データ
{
  "id": 123,
  "name": "Alice",
  "email": "alice@example.com",
  "role": "user",
  "avatar": "default.png"
}

# PUT: 全体置換(送信されていないフィールドはリセット)
PUT /users/123
Body: { "name": "Alice Updated", "email": "alice@new.com" }
# 結果: roleとavatarがnull/デフォルトに!

# PATCH: 部分更新(送信されたフィールドのみ変更)
PATCH /users/123
Body: { "name": "Alice Updated" }
# 結果: nameのみ変更、他のフィールドは保持

4. ステータスコード

4.1 主要ステータスコード

2xx 成功:
200 OK                  - 一般的な成功(GET, PUT, PATCH, DELETE201 Created             - リソース作成成功(POST+ Locationヘッダー
202 Accepted            - 非同期処理受付
204 No Content          - 成功だがレスポンスボディなし(DELETE
3xx リダイレクト:
301 Moved Permanently   - 恒久的リダイレクト
302 Found               - 一時的リダイレクト
304 Not Modified        - キャッシュされたバージョンを使用

4xx クライアントエラー:
400 Bad Request         - 無効なリクエスト(バリデーション失敗)
401 Unauthorized        - 認証が必要(未ログイン)
403 Forbidden           - 認可失敗(権限なし)
404 Not Found           - リソースが存在しない
405 Method Not Allowed  - サポートされていないHTTPメソッド
409 Conflict            - リソースの競合(重複作成)
422 Unprocessable Entity - 構文は正しいが意味が不正
429 Too Many Requests   - レートリミット超過

5xx サーバーエラー:
500 Internal Server Error - サーバーエラー
502 Bad Gateway          - 上流サーバーエラー
503 Service Unavailable  - サービス一時停止
504 Gateway Timeout      - 上流サーバータイムアウト

4.2 ステータスコード選択ガイド

POST リソース作成成功 -> 201 Created
  レスポンスヘッダー:
    Location: /users/124
  レスポンスボディ:
    { "id": 124, "name": "Bob", ... }

DELETE 成功:
  方法1: 204 No Content(ボディなし)
  方法2: 200 OK + 削除されたリソース情報

認証 vs 認可:
  未ログイン -> 401 Unauthorized
  ログイン済みだが権限なし -> 403 Forbidden

バリデーション失敗:
  JSONパース失敗 -> 400 Bad Request
  フィールド値が不正 -> 422 Unprocessable Entity

リソースの競合:
  メール重複 -> 409 Conflict
  楽観的ロック失敗 -> 409 Conflict

5. エラーレスポンス設計

5.1 RFC 7807 Problem Details

{
  "type": "https://api.example.com/errors/validation-error",
  "title": "Validation Error",
  "status": 422,
  "detail": "One or more fields failed validation.",
  "instance": "/users/123",
  "errors": [
    {
      "field": "email",
      "code": "INVALID_FORMAT",
      "message": "有効なメールアドレスを入力してください"
    },
    {
      "field": "age",
      "code": "OUT_OF_RANGE",
      "message": "0から150の間で入力してください"
    }
  ],
  "timestamp": "2025-03-25T10:30:00Z",
  "trace_id": "abc-123-def-456"
}

5.2 エラーコード体系

エラーコードの命名規則:

AUTH_001  - 認証トークン期限切れ
AUTH_002  - 無効な資格情報
AUTH_003  - アカウントロック

USER_001  - ユーザーが見つからない
USER_002  - メールアドレスが既に存在
USER_003  - パスワードポリシー違反

ORDER_001 - 在庫不足
ORDER_002 - 決済失敗
ORDER_003 - 注文は既にキャンセル済み

RATE_001  - APIコール制限超過
RATE_002  - 日次制限超過

5.3 エラーレスポンスの実装

from flask import Flask, jsonify
from werkzeug.exceptions import HTTPException

app = Flask(__name__)

class APIError(Exception):
    def __init__(self, status, error_type, title, detail, errors=None):
        self.status = status
        self.error_type = error_type
        self.title = title
        self.detail = detail
        self.errors = errors or []

@app.errorhandler(APIError)
def handle_api_error(error):
    response = {
        "type": f"https://api.example.com/errors/{error.error_type}",
        "title": error.title,
        "status": error.status,
        "detail": error.detail,
    }
    if error.errors:
        response["errors"] = error.errors
    return jsonify(response), error.status

@app.errorhandler(404)
def not_found(e):
    return jsonify({
        "type": "https://api.example.com/errors/not-found",
        "title": "Resource Not Found",
        "status": 404,
        "detail": "リクエストされたリソースは存在しません。"
    }), 404

6. ページネーション

6.1 Offsetベース

GET /users?page=3&per_page=20

レスポンス:
{
  "data": [...],
  "pagination": {
    "page": 3,
    "per_page": 20,
    "total_items": 1543,
    "total_pages": 78,
    "has_next": true,
    "has_prev": true
  }
}

メリット: 実装が簡単、特定ページへの直接ジャンプが可能
デメリット: 大量データで遅い(OFFSET 10000)、データ変更時に重複/欠落

Offsetの問題点:

-- page=500, per_page=20の場合
SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 9980;
-- DBが9980行を読み込んで破棄し、20行のみ返す -> 非常に非効率!

6.2 Cursorベース(推奨)

GET /users?limit=20&cursor=eyJpZCI6MTIzfQ==

レスポンス:
{
  "data": [...],
  "pagination": {
    "next_cursor": "eyJpZCI6MTQzfQ==",
    "prev_cursor": "eyJpZCI6MTI0fQ==",
    "has_next": true,
    "has_prev": true,
    "limit": 20
  }
}

Cursorの実装:

import base64
import json

def encode_cursor(last_item):
    """最後のアイテムのソートキーをカーソルとしてエンコード"""
    cursor_data = {"id": last_item["id"], "created_at": last_item["created_at"]}
    return base64.urlsafe_b64encode(
        json.dumps(cursor_data).encode()
    ).decode()

def decode_cursor(cursor_str):
    """カーソルをデコードしてソートキーを抽出"""
    return json.loads(
        base64.urlsafe_b64decode(cursor_str.encode()).decode()
    )

def get_users(cursor=None, limit=20):
    query = "SELECT * FROM users"
    if cursor:
        decoded = decode_cursor(cursor)
        query += f" WHERE id > {decoded['id']}"
    query += f" ORDER BY id ASC LIMIT {limit + 1}"

    results = db.execute(query)
    has_next = len(results) > limit
    items = results[:limit]

    return {
        "data": items,
        "pagination": {
            "next_cursor": encode_cursor(items[-1]) if has_next else None,
            "has_next": has_next,
            "limit": limit
        }
    }

6.3 Keysetベース

-- Keyset PaginationはCursorの核心
-- 複合ソートキーの例

-- 最初のページ
SELECT * FROM orders
ORDER BY created_at DESC, id DESC
LIMIT 20;

-- 次のページ(最後のアイテム: created_at='2025-03-20', id=456)
SELECT * FROM orders
WHERE (created_at, id) < ('2025-03-20', 456)
ORDER BY created_at DESC, id DESC
LIMIT 20;

-- 適切なインデックスがあれば非常に高速!
CREATE INDEX idx_orders_cursor ON orders(created_at DESC, id DESC);

6.4 ページネーション比較

方式パフォーマンス一貫性直接ページジャンプ複雑度
Offset大量データで遅いデータ変更時に問題可能
Cursor安定したパフォーマンス一貫性を維持不可
Keyset非常に高速一貫性を維持不可

7. フィルタリング、ソート、フィールド選択

7.1 フィルタリング

# 基本フィルタリング
GET /products?category=electronics&status=available

# 範囲フィルター
GET /products?min_price=100&max_price=500
GET /orders?created_after=2025-01-01&created_before=2025-03-25

# 複数値フィルター
GET /products?tags=phone,tablet,laptop

# 検索
GET /products?q=wireless+keyboard

# 複雑なフィルター(上級)
GET /products?filter[category]=electronics&filter[price][gte]=100&filter[price][lte]=500

7.2 ソート

# 単一フィールドソート
GET /users?sort=name          # 昇順
GET /users?sort=-name         # 降順

# 複数フィールドソート
GET /users?sort=-created_at,name

# JSON:APIスタイル
GET /articles?sort=-published_at,title

7.3 フィールド選択(Sparse Fieldsets)

# 必要なフィールドのみリクエスト(帯域幅の節約)
GET /users/123?fields=id,name,email

# 関連リソースの埋め込み
GET /users/123?include=orders,profile
GET /articles/456?include=author,comments&fields[articles]=title,body&fields[author]=name

8. バージョニング

8.1 URLパスバージョニング(最も一般的)

GET /v1/users/123
GET /v2/users/123

メリット:
- 明確で直感的
- ブラウザでのテストが簡単
- ルーティングの分離が容易

デメリット:
- URLがリソースの場所を識別するというREST原則に違反
- バージョン変更時に全てのURLが変わる

8.2 ヘッダーバージョニング

# Acceptヘッダー(Content Negotiation)
GET /users/123
Accept: application/vnd.myapi.v2+json

# カスタムヘッダー
GET /users/123
X-API-Version: 2

メリット: クリーンなURLREST原則に準拠
デメリット: テストが不便、デバッグが困難

8.3 バージョン管理戦略

1. 互換性の原則:
   - フィールドの追加は非破壊的(既存クライアントに影響なし)
   - フィールドの削除/名前変更は破壊的 -> 新バージョンが必要
   - フィールドの型変更は破壊的 -> 新バージョンが必要

2. 非推奨化プロセス:
   Phase 1: 新バージョンリリース + 旧版にSunsetヘッダー追加
   Phase 2: 旧バージョンに警告レスポンス追加
   Phase 3: 旧バージョン終了(最低6ヶ月の猶予期間)

   HTTPヘッダー:
   Sunset: Sat, 01 Jan 2026 00:00:00 GMT
   Deprecation: true
   Link: <https://api.example.com/v2>; rel="successor-version"

3. API変更ログ:
   - 全ての変更を文書化
   - マイグレーションガイドの提供
   - クライアントへの事前通知

9. 認証とセキュリティ

9.1 認証方式の比較

方式ユースケースメリットデメリット
API Keyサーバー間、シンプルなAPI実装が簡単権限の細分化が困難
OAuth 2.0ユーザー代理操作標準化、スコープ制御実装が複雑
JWTステートレス認証サーバースケーリングが容易トークン失効が困難
Session従来のWebアプリサーバー側制御が容易スケーラビリティに制限

9.2 JWT(JSON Web Token)

JWT構造:
Header.Payload.Signature

Header:
{
  "alg": "RS256",
  "typ": "JWT"
}

Payload:
{
  "sub": "user-123",
  "email": "alice@example.com",
  "roles": ["admin", "user"],
  "iat": 1711353000,
  "exp": 1711356600
}

Signature:
RS256(base64(header) + "." + base64(payload), privateKey)

JWT使用パターン:

# Access Token + Refresh Token
Access Token: 15分有効
Refresh Token: 7日有効

1. ログイン -> Access + Refresh Tokenを発行
2. API呼び出し -> Authorization: Bearer access_token_value
3. Access Token期限切れ -> Refresh Tokenで更新
4. Refresh Token期限切れ -> 再ログイン

# ヘッダー例
GET /users/me
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

9.3 OAuth 2.0フロー

Authorization Code Flow(Webアプリ):
1. ユーザー -> 認可サーバーにリダイレクト
2. ユーザーがログイン + 同意
3. Authorization codeがコールバックURLに返される
4. Authorization codeをAccess Tokenに交換
5. Access TokenでAPIを呼び出し

Client Credentials Flow(サーバー間通信):
1. Client ID + Secretで直接トークンをリクエスト
2. Access Tokenが発行される
3. APIを呼び出し

9.4 APIセキュリティチェックリスト

1. HTTPS必須(HTTPは絶対NG2. 認証トークンはヘッダーのみ(URLパラメータは不可)
3. 入力バリデーション(SQLインジェクション、XSS防止)
4. CORS設定(許可ドメインのみ)
5. Rate Limitingの適用
6. 機密データのログ出力禁止
7. エラーメッセージに内部情報を含めない
8. APIキーは環境変数で管理
9. 定期的なセキュリティ監査
10. 依存関係の脆弱性スキャン

10. レートリミッティング

10.1 レスポンスヘッダー

HTTP/1.1 200 OK
X-RateLimit-Limit: 1000          # 1時間あたりの最大リクエスト数
X-RateLimit-Remaining: 742       # 残りリクエスト数
X-RateLimit-Reset: 1711360000    # リセット時刻(Unixタイムスタンプ)

# 制限超過時
HTTP/1.1 429 Too Many Requests
Retry-After: 60                  # 60秒後にリトライ
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1711360000

{
  "type": "https://api.example.com/errors/rate-limit-exceeded",
  "title": "Rate Limit Exceeded",
  "status": 429,
  "detail": "1時間あたり1000リクエストの制限を超過しました。",
  "retry_after": 60
}

10.2 レートリミッティング戦略

1. ユーザーごとの制限:
   - 認証済みユーザー: 1000 req/hour
   - 未認証ユーザー: 100 req/hour

2. エンドポイントごとの制限:
   - GET /users: 100 req/min
   - POST /users: 10 req/min
   - POST /payments: 5 req/min

3. ティアベースの制限:
   - Free: 100 req/day
   - Basic: 10,000 req/day
   - Pro: 100,000 req/day
   - Enterprise: カスタム

4. 実装(Redis):
   key = "rate:user:123:2025-03-25-10"  # ユーザー + 時間ウィンドウ
   INCR key
   EXPIRE key 3600

11. HATEOAS

11.1 HATEOASとは

{
  "id": 123,
  "name": "Alice",
  "email": "alice@example.com",
  "status": "active",
  "_links": {
    "self": {
      "href": "/users/123"
    },
    "orders": {
      "href": "/users/123/orders"
    },
    "deactivate": {
      "href": "/users/123/deactivate",
      "method": "POST"
    },
    "update": {
      "href": "/users/123",
      "method": "PUT"
    }
  }
}

11.2 HATEOASのメリットとデメリット

メリット:
- APIが探索可能(自己文書化)
- クライアントがURLをハードコードする必要がない
- サーバーがURL構造を変更可能
- レスポンスから利用可能なアクションを発見

デメリット:
- レスポンスサイズの増加
- 実装の複雑さ
- ほとんどのクライアントが活用しない
- 実務では必須ではない

結論: パブリックAPIでは推奨、内部APIではオプション

12. バルク操作と非同期処理

12.1 バッチエンドポイント

// POST /users/batch
{
  "operations": [
    { "method": "POST", "body": { "name": "Alice", "email": "a@ex.com" } },
    { "method": "POST", "body": { "name": "Bob", "email": "b@ex.com" } },
    { "method": "POST", "body": { "name": "Carol", "email": "c@ex.com" } }
  ]
}

// レスポンス: 207 Multi-Status
{
  "results": [
    { "status": 201, "data": { "id": 1, "name": "Alice" } },
    { "status": 201, "data": { "id": 2, "name": "Bob" } },
    { "status": 409, "error": { "code": "USER_002", "message": "メールアドレスが既に存在します" } }
  ]
}

12.2 非同期処理パターン

# 長時間タスク(レポート生成、バルクデータ処理)

POST /reports
Body: { "type": "annual", "year": 2025 }

レスポンス: 202 Accepted
{
  "task_id": "task-abc-123",
  "status": "processing",
  "estimated_time": 120,
  "_links": {
    "status": { "href": "/tasks/task-abc-123" },
    "cancel": { "href": "/tasks/task-abc-123/cancel", "method": "POST" }
  }
}

# ステータス確認
GET /tasks/task-abc-123

{
  "task_id": "task-abc-123",
  "status": "completed",
  "result": {
    "download_url": "/reports/2025-annual.pdf"
  },
  "completed_at": "2025-03-25T10:35:00Z"
}

13. OpenAPI 3.1仕様

13.1 OpenAPIの基本構造

openapi: "3.1.0"
info:
  title: ユーザー管理API
  description: ユーザー管理のためのAPI
  version: "1.0.0"
  contact:
    email: api@example.com

servers:
  - url: https://api.example.com/v1
    description: 本番環境
  - url: https://staging-api.example.com/v1
    description: ステージング環境

paths:
  /users:
    get:
      summary: ユーザー一覧取得
      operationId: listUsers
      tags:
        - Users
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        "200":
          description: 成功レスポンス
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/User"
                  pagination:
                    $ref: "#/components/schemas/Pagination"

    post:
      summary: ユーザー作成
      operationId: createUser
      tags:
        - Users
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateUserRequest"
      responses:
        "201":
          description: ユーザー作成成功
          headers:
            Location:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "422":
          description: バリデーションエラー
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
          format: email
      required:
        - id
        - name
        - email

    CreateUserRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
        email:
          type: string
          format: email
      required:
        - name
        - email

13.2 OpenAPIコード生成

# OpenAPIからコードを自動生成

1. クライアントSDK生成:
   npx openapi-generator-cli generate \
     -i openapi.yaml \
     -g typescript-fetch \
     -o ./generated-client

2. サーバースタブ生成:
   npx openapi-generator-cli generate \
     -i openapi.yaml \
     -g python-flask \
     -o ./generated-server

3. モックサーバー:
   npx prism mock openapi.yaml

4. APIドキュメント:
   npx redocly build-docs openapi.yaml

14. API設計のアンチパターン

14.1 よくある10の間違い

1. URLに動詞を使用
   Bad:  POST /createUser
   Good: POST /users

2. 一貫性のないネーミング
   Bad:  /users, /getOrders, /product-list
   Good: /users, /orders, /products

3. 単数形のリソース名
   Bad:  /user/123
   Good: /users/123

4. HTTPメソッドの無視
   Bad:  POST /users/123/delete
   Good: DELETE /users/123

5. 常に200を返す
   Bad:  200 OK { "error": true, "message": "Not found" }
   Good: 404 Not Found { "title": "Not Found", ... }

6. 過度なネスト
   Bad:  /countries/us/cities/nyc/districts/manhattan/restaurants
   Good: /restaurants?city=nyc&district=manhattan

7. バージョニングなしのAPI
   Bad:  /users(いつでも変更される可能性)
   Good: /v1/users

8. エラーレスポンスの標準なし
   Bad:  { "error": "something went wrong" }
   Good: RFC 7807 Problem Details形式

9. ページネーションなし
   Bad:  GET /logs(100万件を返す)
   Good: GET /logs?limit=50&cursor=abc

10. セキュリティヘッダーの欠如
    Bad:  HTTPを許可、CORSを全開放
    Good: HTTPS必須、最小限のCORS、適切なヘッダー

15. GraphQL vs REST vs gRPC比較

+--------------+-------------------+-------------------+-------------------+
|   機能       |      REST         |     GraphQL       |      gRPC         |
+--------------+-------------------+-------------------+-------------------+
| プロトコル   | HTTP/1.1, 2       | HTTP/1.1, 2       | HTTP/2            |
| データ形式   | JSON/XML          | JSON              | Protocol Buffers  |
| スキーマ     | OpenAPI(任意)   | SDL(必須)       | .proto(必須)    |
| Over/Under   | Over-fetching     | クライアントが    | 正確なスキーマ    |
|   fetching   | の可能性          | 決定              |                   |
| リアルタイム | WebSocket/SSE     | Subscriptions     | 双方向            |
|              |                   |                   | ストリーミング    |
| ユースケース | パブリックAPI| 複雑なデータ      | マイクロサービス  |
|              | シンプルなCRUD    | 関係、モバイル    | 内部通信          |
| 学習コスト   ||||
| キャッシュ   | HTTPキャッシュ容易| 困難              | なし              |
| エラー       | HTTPステータス    | errorsの配列      | ステータスコード  |
+--------------+-------------------+-------------------+-------------------+

選択ガイド:

RESTを選択:
- パブリックAPI(外部開発者向け)
- シンプルなCRUD操作
- HTTPキャッシュが必要
- ブラウザからの直接呼び出し

GraphQLを選択:
- 複数のクライアント(Web、モバイル、IoT)
- 複雑なデータ関係
- Over/Under-fetchingの解決が必要
- フロントエンドの迅速な開発

gRPCを選択:
- 内部マイクロサービス通信
- 低レイテンシ要件
- 双方向ストリーミングが必要
- 強い型付け契約が必要

16. クイズ

Q1. REST原則

「ステートレス」制約がスケーラビリティに貢献する理由を説明してください。

回答: ステートレス制約はサーバーがクライアントの状態を保存しないことを意味し、各リクエストは処理に必要な全ての情報を含みます。これにより:

  1. シンプルなロードバランシング: どのサーバーでもどのリクエストでも処理可能(セッションスティッキネス不要)
  2. 容易な水平スケーリング: サーバーの追加/削除が自由
  3. 容易な障害復旧: サーバー障害時に別のサーバーが即座に引き継ぎ
  4. キャッシュ効率の向上: リクエストが独立しており、キャッシュキー生成が簡単

Q2. ページネーション

Offsetページネーションではなく、Cursorページネーションを選択すべき2つの状況を説明してください。

回答:

  1. 大量データ: 数百万件のレコードがある場合、OffsetはOFFSET 100000のように多くの行をスキップする必要があり非常に遅い。CursorはWHERE句でインデックスを使用して直接ジャンプし、データ量に関係なく一定のパフォーマンスを維持。

  2. リアルタイムデータ: データが頻繁に追加/削除される場合、Offsetは同じページを再リクエストすると重複や欠落が発生。Cursorは最後に見た項目の一意の識別子を基準点として使用し、一貫性を維持。

Q3. エラー処理

401 Unauthorizedと403 Forbiddenの違いを具体例で説明してください。

回答:

  • 401 Unauthorized(認証失敗): クライアントが自分が誰であるかを証明していない。例えば、Authorizationヘッダーなしでのリクエストや、期限切れのJWTトークンの使用。「あなたが誰かわかりません。ログインしてください。」

  • 403 Forbidden(認可失敗): クライアントは認証済みだが、リソースへの権限がない。例えば、一般ユーザーが管理者専用APIを呼び出す場合。「あなたが誰かは知っていますが、このリソースへのアクセス権限がありません。」

Q4. バージョニング

URLパスバージョニング(/v1/users)とAcceptヘッダーバージョニングのメリットとデメリットを比較してください。

回答: URLパスバージョニング(/v1/users):

  • メリット:直感的、ブラウザテストが簡単、ルーティング分離が容易、キャッシュが簡単
  • デメリット:REST原則に違反(URLはリソースの場所を識別すべき)、全URLが変更

Acceptヘッダーバージョニング(Accept: application/vnd.myapi.v2+json):

  • メリット:クリーンなURL、REST原則に準拠、1つのURLで複数バージョン
  • デメリット:テストが不便(curl/Postmanが必要)、デバッグが困難、キャッシュが複雑

実務ではURLパスバージョニングが圧倒的に主流(GitHub、Stripe、Googleなど)。

Q5. セキュリティ

JWTのメリット/デメリットとトークン盗難時の対応について説明してください。

回答: メリット: サーバー側のセッション保存不要(ステートレス)、水平スケーリングが容易、マイクロサービス間の伝播が簡単、Claimsにユーザー情報を含められる

デメリット: トークンの取消が困難(有効期限まで有効)、セッションIDより大きい、Payloadは暗号化されない(エンコードのみ)

トークン盗難時の対応:

  1. Access Tokenの有効期間を短く(15分)
  2. Refresh Tokenのローテーション(使用時に新しいトークンを発行、古いものを無効化)
  3. トークンブラックリスト(取消されたトークンをRedisに保存)
  4. IP/User-Agentのバインド
  5. 不審な活動を検知した際に全Refresh Tokenを無効化

参考資料

  1. RFC 7231 - HTTP/1.1 Semantics and Content - https://tools.ietf.org/html/rfc7231
  2. RFC 7807 - Problem Details for HTTP APIs - https://tools.ietf.org/html/rfc7807
  3. OpenAPI Specification 3.1 - https://spec.openapis.org/oas/v3.1.0
  4. JSON:API Specification - https://jsonapi.org/
  5. Google API Design Guide - https://cloud.google.com/apis/design
  6. Microsoft REST API Guidelines - https://github.com/microsoft/api-guidelines
  7. Stripe API Reference - https://stripe.com/docs/api
  8. GitHub REST API - https://docs.github.com/en/rest
  9. Zalando RESTful API Guidelines - https://opensource.zalando.com/restful-api-guidelines/
  10. REST API Design Rulebook - Mark Masse (O'Reilly)
  11. OAuth 2.0 RFC 6749 - https://tools.ietf.org/html/rfc6749
  12. JWT RFC 7519 - https://tools.ietf.org/html/rfc7519
  13. Roy Fielding's Dissertation - https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
  14. Richardson Maturity Model - https://martinfowler.com/articles/richardsonMaturityModel.html