冪等性とバージョン管理されたREST APIの設計方法
新機能をデプロイした途端、タイムアウト後にクライアントがリトライしたことで、APIに重複したPOSTリクエストが届くようになりました。データベースには同一の注文が2件存在しています。あるいは破壊的変更をリリースしたら、古いレスポンス形式を期待していたモバイルアプリがクラッシュしました。これらはAPI設計において最も一般的で厄介な問題の2つです:非冪等な操作と破壊的変更です。このガイドでは、REST APIにおける冪等性とバージョン管理の実践的なパターンを用いて、両方を解決する方法を紹介します。
REST APIにおいて冪等性が重要な理由
冪等性とは、同じリクエストを複数回行っても、1回行った場合と同じ結果になることを意味します。HTTPメソッドには定義された冪等性のセマンティクスがあります:GET、HEAD、PUT、DELETE、OPTIONSは冪等ですが、POSTとPATCHは冪等ではありません。しかし実際のAPIでは、リソースの作成や決済処理などの操作でPOSTを受け入れる必要があることがよくあります。ネットワーク障害、クライアントのタイムアウト、リトライロジックは重複を引き起こす可能性があります。冪等性がなければ、二重請求、重複レコード、状態の不整合といったリスクがあります。
冪等なREST APIのためのテクニック
1. POSTリクエストに冪等性キーを使用する
最も一般的なアプローチは、クライアントが一意のキー(例:UUID)をIdempotency-Keyヘッダーで送信できるようにすることです。サーバーはキーと操作の結果を保存します。同じキーを再度受け取った場合、サーバーは操作を再実行せずに保存された結果を返します。
POST /payments
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"amount": 1000,
"currency": "USD"
}
実装手順:
- クライアントは論理的な操作ごとに一意のキー(UUID v4)を生成します。
- サーバーは永続ストア(例:Redisやデータベース)にキーが存在するか確認します。
- 存在しない場合、リクエストを処理し、レスポンスのステータスとボディと共にキーを保存します。
- 存在する場合、再処理せずに保存されたレスポンスを返します。
ストレージが無制限に増えないよう、適切な有効期限(例:24時間)を設定してください。
2. 条件付きリクエストを活用する
更新には、ETagとIf-Matchヘッダーを使用して更新の消失を防ぎます。サーバーは現在の状態を表すETagを返します。クライアントはそれをIf-Matchと共に送り返し、リソースが変更されていた場合はサーバーが412 Precondition Failedを返します。これにより、PUTリクエストが競合状態に対して安全になります。
PUT /articles/123
If-Match: "abc123"
Content-Type: application/json
{"title": "Updated title"}
3. 自然な冪等性のためにPUTを設計する
クライアントがリソースURLを決定できる場合は、POSTよりPUTを優先してください。例えば、PUT /users/{userId}は同じリソースを上書きするため、本質的に冪等です。POSTはサーバーがIDを割り当てる場合にのみ使用してください。
4. 一意制約による重複検出を処理する
冪等性キーと、ビジネスキー(例:注文番号)に対するデータベースの一意制約を組み合わせます。重複がすり抜けた場合、データベースがそれを拒否し、明確なメッセージと共に409 Conflictを返すことができます。
REST APIのバージョン管理戦略
APIは進化します。新しいフィールド、変更された動作、削除されたエンドポイントは既存のクライアントを壊す可能性があります。バージョン管理により、クライアントを中断させることなく変更を導入できます。一般的な戦略がいくつかあります:
| 戦略 | 例 | 長所 | 短所 |
|---|---|---|---|
| URIパス | /v1/users |
明示的、ルーティングが容易、キャッシュフレンドリー | URLが変わる、煩雑になりうる |
| クエリパラメータ | /users?version=1 |
シンプル、任意指定可能 | 省略されやすい、RESTfulでない |
| カスタムヘッダー | Accept-Version: v1 |
URLをクリーンに保てる | ブラウザでのテストが難しい |
| メディアタイプ | Accept: application/vnd.api.v1+json |
コンテンツネゴシエーション、純粋なREST | 複雑、あまり一般的でない |
ほとんどのチームにとって、URIパスバージョニングが最も実用的な選択です。可視性が高く、ドキュメント化が容易で、APIゲートウェイやCDNともうまく機能します。
クライアントを壊さずにバージョン管理する方法
- 最初からパスにv1を含める:後からバージョン管理を追加するのは困難です。
- 同一バージョン内では追加的な変更のみ:新しい任意フィールド、新しいエンドポイント。フィールドの削除や名前変更は決して行わないこと。
- 段階的に非推奨化する:サポート終了を告知し、移行ガイドを提供し、旧バージョンの使用状況を監視します。
- Sunsetヘッダーを使用する:
Sunset: Sat, 31 Dec 2025 23:59:59 GMTでクライアントに通知します。 - アクティブなバージョンは最大2つまで維持する:メンテナンス負担を抑えます。
まとめ:実践的な例
決済APIを考えてみましょう。決済を冪等に作成し、エンドポイントをバージョン管理したいとします。
POST /v1/payments
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"amount": 1000,
"currency": "USD"
}
サーバーのロジック:
- Redisに
Idempotency-Keyが存在するか確認します。 - 存在する場合、キャッシュされたレスポンス(ステータス+ボディ)を返します。
- 存在しない場合、決済を処理し、レスポンスと共にキーを保存し、201 Createdを返します。
更新には、ETag付きのPUTを使用します:
PUT /v1/payments/123
If-Match: "xyz789"
Content-Type: application/json
{"amount": 1500}
ETagが一致しない場合は412を返します。これにより、同時変更の上書きを防ぎます。
よくある落とし穴と回避方法
- 冪等性キーを永久に保存する:ストレージの肥大化を避けるためTTL(例:24時間)を設定します。
- 競合状態を無視する:キーの確認と設定にはアトミック操作(例:Redis SETNX)を使用します。
- バージョン管理が遅すぎる:最初から/v1を追加します。
- マイナーバージョンでの破壊的変更:レスポンス構造を変える変更はすべてメジャーとして扱います。
- 冪等性を文書化しない:どのエンドポイントがIdempotency-Keyをサポートするかを明確に記載します。
FAQ
冪等なREST APIとは何ですか?
冪等なREST APIとは、同じリクエストを複数回行っても1回行った場合と同じ結果になることを保証するものです。これは、特にPOSTのような非冪等メソッドにおいて、リトライを安全に処理するために重要です。
どのHTTPメソッドが冪等ですか?
GET、HEAD、PUT、DELETE、OPTIONS、TRACEは冪等です。POSTとPATCHはデフォルトでは冪等ではありませんが、冪等性キーなどのテクニックを使って冪等にすることができます。
REST APIをバージョン管理する最良の方法は何ですか?
URIパスバージョニング(例:/v1/users)が最も一般的で実用的なアプローチです。明示的で、ルーティングが容易で、キャッシュやAPIゲートウェイともうまく機能します。他にもクエリパラメータ、カスタムヘッダー、メディアタイプなどの選択肢がありますが、それぞれトレードオフがあります。
APIのJSONレスポンスをテストする準備はできましたか?JSON Formatterを使って、ペイロードをすばやく検証・整形しましょう。