APIバージョン管理戦略
TL;DR: 日付ベースでAPIキーごとにバージョンを固定する(Stripeのモデル)ことは、長期運用されるパブリックAPIにおいて
/v1/、/v2/よりも優れています。なぜなら、クライアントに新しいツリーへの移行を強制せずに、小さな破壊的変更を行えるからです。内部APIの場合は、追加的な進化とDeprecationおよびSunsetヘッダー(RFC 9745、RFC 8594)で十分です。常に事前に告知し、常に日付を決めて廃止し、決して単に削除しないでください。
すぐに知りたいことから選ぶ
| 症状 | セクション |
|---|---|
| 「フィールドを変更したいけど、10万のAPIキーがある」 | 日付ベース固定バージョン |
「/v1/ vs Accept: application/vnd.foo.v2+json どちらを選ぶべき?」 | 戦略比較 |
| 「エンドポイントが廃止されることをクライアントに伝えるには?」 | 非推奨+廃止 |
| 「コードで6つのバージョンを管理するのが大変」 | バージョントランスフォーマー |
| 「内部API — バージョン管理は本当に必要?」 | 内部API vs パブリックAPI |
決定図
flowchart TD
A[APIの動作を変更する必要がある] --> B{追加のみ?<br/>新しいフィールド、新しいエンドポイント、新しいオプションのパラメータ}
B -->|はい| C[そのままリリース<br/>バージョン管理不要]
B -->|いいえ、破壊的変更| D{APIはパブリック<br/>+ 不透明なクライアントが多い?}
D -->|いいえ、内部/クライアント少数| E[移行を調整<br/>+ Deprecation/Sunset ヘッダー]
D -->|はい、パブリック| F{リクエストごとに<br/>互換レイヤーを適用できる?}
F -->|はい、トランスフォーマーあり| G[日付ベース固定バージョン<br/>Stripeモデル]
F -->|いいえ、大規模な書き換え| H[メジャーバージョンURI更新<br/>/v1 → /v2]
E --> I[Deprecation: @timestamp<br/>+ Sunset: HTTP-date<br/>+ Link: rel=deprecation を設定]
G --> I
H --> I
1. 日付ベース固定バージョン(Stripeのモデル)
StripeはAPIバージョニングのブログ記事でそのアーキテクチャを公開しています:
ユーザーが初めてAPIリクエストを行うと、そのアカウントは自動的にその時点で利用可能な最新バージョンに固定されます。以降、そのユーザーが行うすべてのAPI呼び出しには、そのバージョンが暗黙的に割り当てられます。
[バージョンは]リリースされた日付で名前が付けられたローリングバージョンです(例:
2017-05-24)。
主な特性:
| プロパティ | なぜうまくいくか |
|---|---|
| アカウントごとのデフォルトバージョン(最初の呼び出し時に設定) | 新規顧客は自動的に最新バージョンに固定され、変更をリリースしても既存顧客は影響を受けない |
Stripe-Versionヘッダーによるリクエストごとの上書き | 段階的なクライアント移行が可能:SDKはアカウントのデフォルトより前に新しいバージョンを試せる |
日付ベースの名前(2024-04-10) | 新しさが伝わる。「v3」の意味について議論が不要。大きなv3まで貯めずに、小さな変更を何度でも行える |
| ダッシュボードによるアップグレードパス | 顧客は差分をプレビューし、アカウントのデフォルトバージョンをアップグレードできる |
これは、真に長期運用されるパブリックAPI(Stripeは10年前のバージョンも維持)にスケールする唯一のアプローチです。内部の複雑さ(§4のトランスフォーマーパターン)がかかりますが、顧客に永続的な/v1/ → /v2/ 移行サイクルを強いることを避けられます。
2. バージョン管理戦略比較
| 戦略 | バージョンの配置場所 | メリット | デメリット | 最適な用途 |
|---|---|---|---|---|
URIセグメント(/v1/、/v2/) | パス | キャッシュフレンドリー(URLが異なるとキャッシュエントリも異なる)。クライアント設定不要 | 小さな変更ができない。ツリーのフォークを強制。FieldingによるとURLが「安定したリソース識別子」でなくなる | 内部API、稀にしか変更しないパブリックAPI |
カスタムヘッダー(Stripe-Version: 2024-04-10) | リクエストヘッダー | 多くのバージョンを安価に扱える。リクエストごとの細かい粒度 | URLにバージョンを含められない。ブラウザのアドレスバーでテストしにくい | 大規模なパブリックAPI |
Acceptメディアタイプ(Accept: application/vnd.foo.v2+json) | 標準のAcceptヘッダー | HTTPの仕様に準拠。コンテンツネゴシエーションの仕組みを再利用 | 設定が面倒。ツールのサポートにばらつきがある。curlでのデバッグが冗長 | ハイパーメディアAPI、深いRESTful設計 |
クエリパラメータ(?version=2) | URL | テストが簡単。ログに表示される | URLを汚染する。意味的に間違っている(バージョンはリソースのプロパティではない) | 迅速なプロトタイピングのみ |
| バージョン管理なし、追加のみ | なし | オーバーヘッドゼロ。クライアントの調整不要 | 新しいエンドポイントなしでは破壊的変更が一切できない | 共有デプロイの内部マイクロサービス |
制約に応じて選択:
- 「頻繁に小さな変更を行う」→ 日付ベースヘッダー(Stripe)
- 「5年に一度大きな書き換えをする」→ URIセグメント(
/v1/、/v2/) - 「決して変更せず、追加のみ」→ バージョン管理なし+追加的進化
3. Deprecation/Sunset ヘッダーペア
2つのIETF RFCがライフサイクルシグナルをカバーしています:
Sunset ヘッダー — RFC 8594
Sunset値は、RFC 7231のセクション7.1.1.1で定義されているHTTP-dateタイムスタンプであり、将来のタイムスタンプであるべきです(SHOULD)。
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
「リソースが将来の特定の時点で応答しなくなることが予想される」ことを示します。クライアントはこのタイムスタンプをヒントとして扱うべきであり(SHOULD)、厳格な契約として扱うべきではありません。
Deprecation ヘッダー — RFC 9745
Deprecation: @1735689599
Sunset: Sun, 31 Dec 2026 23:59:59 GMT
Link: <https://api.example.com/docs/migrate-v1-to-v2>; rel="deprecation"
Deprecationの値はUnixタイムスタンプ(秒、構造化フィールドに従い@を接頭辞)です。過去(「すでに非推奨」)でも未来(「非推奨になる予定」)でも構いません。必須事項(MUST):
- RFC 9651に従い、構造化フィールドの日付形式を使用しなければならない(MUST)
SunsetはDeprecationより前であってはならない(MUST NOT) — 仕様は明示的。これは時間的な順序の制約rel="deprecation"を持つ**Linkを含めるべき(SHOULD)**、移行ドキュメントを指すこと
Deprecationを送信してもリソースの動作は変わりません — これはシグナルであり、機能低下ではありません。サーバーは、緊急時を除き、Sunset日付までリソースを機能させ続けるべきです(SHOULD)。
推奨タイムライン
T+0 最初の非推奨通知: ヘッダーを本番環境で送信開始
T+30d 公開告知(変更履歴、メール、ドキュメント)
T+90d 非推奨パスの使用状況に関するログ/メトリクスを追加
T+180d 廃止日を90日後に設定
T+270d 廃止日到来 — エンドポイントは移行リンクとともに410 Goneを返す
メジャーバージョン固定APIでの軽微な破壊的変更の場合:中央値は6ヶ月。完全なv1→v2の廃止の場合:12〜24ヶ月。
4. バージョントランスフォーマーパターン
Stripeの内部アーキテクチャは、「本番環境に50のバージョンがある」問題を解決しています:
[システムは]APIリソースクラス(現在のAPI応答構造を定義)と、バージョン変更モジュール(後方互換性のない変換をカプセル化)を組み合わせています。応答を処理する際、システムは時間を逆向きにたどり、対象のバージョンに到達するまで各バージョン変更モジュールを適用します。
形は以下の通り:
// resource: 正規(最新)の表現
const charge = {
id: 'ch_123',
amount: 1000,
payment_method_details: { card: { brand: 'visa', last4: '4242' } },
}
// 各破壊的変更は1つのトランスフォーマーモジュール:
const v_2024_03_01 = {
// payment_method_details下の'card'の入れ子を削除し、フィールドをフラット化
apply(resource) {
return {
...resource,
card_brand: resource.payment_method_details?.card?.brand,
card_last4: resource.payment_method_details?.card?.last4,
}
},
}
const v_2023_10_15 = {
// 'amount'を'amount_cents'にリネーム
apply(resource) {
const { amount, ...rest } = resource
return { ...rest, amount_cents: amount }
},
}
// パイプラインはトランスフォーマーを逆時系列順に適用
// 要求されたバージョンに達するまで
function transform(resource, requestedVersion) {
const chain = transformers.filter(t => t.date > requestedVersion)
return chain.reduceRight((r, t) => t.apply(r), resource)
}
メリット:
- コアコードは最新のまま — すべてのエンドポイントは最新のスキーマに対して記述。ビジネスロジックに
if (version < ...)が散らばらない。 - 各変更は1ファイル — レビューが容易、単独でテストが容易、最後のユーザーがそのバージョンを超えたら削除が容易。
- テレメトリー — 各トランスフォーマーが1日に何回起動するかをカウントし、実際の使用状況に基づいて非推奨化を計画できる。
コスト:トランスフォーマーフレームワークの構築。年間3回以上の破壊的変更があり、1万以上のAPIコンシューマーがいる場合に価値があります。50未満の顧客しかいない内部APIには過剰です。
5. 内部API vs パブリックAPI
非対称性が重要です:
| 内部 | パブリック | |
|---|---|---|
| 調整されたデプロイが可能? | はい — アトミックスワップ | いいえ — クライアントは独立してデプロイ |
| スキーマ進化 | 追加的+Slackメッセージ | 厳格なバージョン管理+メール+ダッシュボード+ブログ投稿 |
| 廃止期間 | 数日〜数週間 | 6〜24ヶ月 |
| バージョン管理の必要性 | 多くの場合不要 — Deprecationヘッダーだけで十分 | 必須 |
| 最適な戦略 | 追加的進化。書き換え時のみURIバージョン | 日付固定ヘッダー(規模に見合う場合)、大規模書き換えにはURI |
落とし穴:すべてのAPIを「パブリック」とみなし、3つの内部サービスだけが使用するAPIに重いバージョン管理インフラを構築してしまうこと。逆の落とし穴:内部APIが外部に漏れ、実際には変更できなくなること。
防御策:「パブリック」をサービスのバイナリタグにする。パブリックサービスは完全なバージョン管理手続きを取得。内部サービスは取得しないが、誰が呼んでいるかの積極的な監視を取得。
アンチパターン
| アンチパターン | なぜ問題か | 修正方法 |
|---|---|---|
1つの破壊的変更で/v1/を/v2/に上げる | 1つのフィールドのためにクライアントにすべてを移行させる | 日付ベースバージョン、または追加的進化 |
Sunsetヘッダーなしでエンドポイントを削除 | クライアントに予告なく影響が出る。サポートチケットが急増 | 削除には常に90日以上の事前にDeprecationを設定 |
Deprecationを送信するがLinkがない | クライアントは非推奨と分かってもどうすればいいか分からない | 常にLink: <migration-doc>; rel="deprecation"を含める |
| 顧客ごとにハードコードされたバージョン例外 | メンテナンス不能。動作を推論できない | トランスフォーマーパターンを使用。「例外」はバージョン固定として扱う |
| 内部APIをパブリックAPIのようにバージョン管理 | 見返りのないプロセスオーバーヘッド | 追加的進化+アトミックデプロイ+Slack |
REST APIでセマンティックバージョニング(v1.2.3)を使用 | 当てはまらない意味を暗示する(JSONスキーマにとって「パッチ」とは?) | 日付ベースの名前、またはメジャーのみ(v1) |
| 強制のない廃止日 | エンドポイントが永遠に存続、コードが腐る | カレンダーリマインダー、廃止日の7日前に失敗するCIテスト |
初心者/エキスパート/タイムライン
| 初心者 | エキスパート | |
|---|---|---|
| 最初の破壊的変更 | /v1を/v2に上げ、ツリー全体をコピーペースト | 日付ベース固定を検討し、変更が実際に破壊的かを評価 |
| クライアントへの非推奨通知 | メール一斉送信 | Deprecation+Sunsetヘッダー+Link+メール+ダッシュボードバナー |
| 5つのバージョンの管理 | 各コントローラーに5つのif/else分岐 | トランスフォーマーモジュール、最新のみのコアコード |
| 廃止の強制 | 「そのうちやる」 | CIに廃止日。その日が来たらエンドポイントは410 Goneを返す |
| 内部APIの変更 | パブリックAPIと同じ手続き — 遅い | 追加的進化+アトミックデプロイ。重いプロセスはパブリックAPIのみに予約 |
タイムラインテスト:「フィールドXを削除したい」から「フィールドXが本番環境から削除された」までどれくらいかかるか? エキスパート組織には単一の反復可能な答えがある(例:90日)。初心者組織にはプロセスがないため答えがない。
品質ゲート
バージョン管理の変更が出荷される条件:
- テスト: 外部から見えるフィールドの削除またはリネームは、少なくともN日前(Nは文書化された廃止期間)の非推奨告知によってゲートされている。
- テスト: すべての非推奨エンドポイントが
Deprecation: @<unix-ts>およびSunset: <HTTP-date>ヘッダーを送信していることを統合テストで確認。 - テスト:
Sunset値がDeprecation値より前になっていないこと(RFC 9745のMUSTに従う)。 - テスト: すべての非推奨告知に
Link: <docs-url>; rel="deprecation"ヘッダーが含まれ、そのURLに公開移行ドキュメントが存在すること(CIでHEADチェック)。 - テスト: バージョンごとの使用状況テレメトリーが存在すること — v2を削除する前に「昨日v2パスへのリクエスト数」に答えられること。
- テスト(日付固定のみ): 新しいAPIキーは最新バージョンがデフォルトになる。既存のキーはデプロイをまたいでも固定を維持する。
- 手動: 廃止強制計画:カレンダーリマインダー、CIチェック、または自動化された410 Gone — 日付が来たときに何かが発火すること。
このスキルの対象外
- GraphQLスキーマ進化(
graphql-schema-evolutionを使用) - gRPC / Protobuf ワイヤー互換性(
protobuf-evolution-rulesを使用) - データベーススキーマバージョン管理(
zero-downtime-database-migrationを使用) - ライブラリ / SDK セマンティックバージョニング(
npm-package-versioningを使用) - イベントストリームスキーマ進化(
kafka-schema-registry-designを使用)
出典
- Stripe: API versioning at Stripe — 固定日付ベースバージョン、トランスフォーマーパターン
- Stripe API: Idempotent requests / versioning policy
- IETF: RFC 8594 — The Sunset HTTP Header Field
- IETF: RFC 9745 — The Deprecation HTTP Header Field
- IETF: RFC 9651 — Structured Field Values for HTTP(Deprecationヘッダーの日付形式)
- Roy Fielding: REST APIs must be hypertext-driven — URIバージョニングに対する標準的な議論