TL;DR
- Versioning 戦略 4 つ: URL Path (
/v1/)、Header (API-Version: 2024-01-01)、Content Negotiation (Accept: application/vnd.api+json;v=1)、Query (?version=1) - Stripe 方式 = 日付ベース:
2024-04-15のように日付でバージョン。最もエレガントなアプローチ - GitHub 方式 = REST URL:
/v3/repos。シンプルだが、major change のたびに migration が必要 - GraphQL = バージョンなし: フィールド単位の deprecation で進化。Twitter、GitHub、Shopify が採用
- Sunset header: RFC 8594。
Sunset: Sat, 31 Dec 2025 23:59:59 GMT— クライアントに廃止スケジュールを通知
1. API 進化の本質的な難しさ
1.1 公開 API の運命
"一度公開された API は永遠に生き続ける。" — Hyrum's Law
内部コードは自由に refactoring できます。外部 API は異なります:
- 数千〜数百万のクライアントが依存
- モバイルアプリはユーザーが update しなければ新バージョンにならない
- 統合されたビジネスシステムは変更に消極的
- 「一時的な停止」が売上の損失に直結
1.2 変更の種類
| 変更 | 影響 | 互換性 |
|---|---|---|
| 新しい endpoint の追加 | なし | OK |
| 新しい response field の追加 | なし | OK |
| optional field の追加 | なし | OK |
| response field の削除 | Breaking | NG |
| field の rename | Breaking | NG |
| field type の変更 | Breaking | NG |
| 必須 field の追加 | Breaking | NG |
| error code の意味変更 | Breaking | NG |
| デフォルト動作の変更 | Breaking | NG |
ルール: 追加は安全、削除/変更は危険。
1.3 Hyrum's Law の残酷さ
"API に十分なユーザーがいれば、どの観測可能な動作も誰かが依存している。"
実際の事例:
- response field の順序を仮定しているクライアント
- error message の正確なテキストを parse しているクライアント
- 副作用 (例: ID が連番であること) に依存するクライアント
- response time が一定であるという仮定
結論: 「公式ドキュメントにないものは変更してよい」は間違いです。観測可能なすべての動作が API の一部です。
2. Versioning 戦略 4 つ
2.1 URL Path Versioning
GET /v1/users/123
GET /v2/users/123
メリット:
- 最も明確
- debug しやすい (URL を見るだけでバージョンが分かる)
- cache 親和性 (異なる URL = 異なる cache)
デメリット:
- major change のたびに新しい URL が必要
- 各バージョンを別のコードベースとして維持
- クライアントが URL を hardcode
採用: GitHub (/v3/)、Twitter、Stripe (URL は /v1/ だが実際は header でバージョン指定)
2.2 Header Versioning
GET /users/123 HTTP/1.1
Stripe-Version: 2024-04-15
メリット:
- URL がきれい
- 同じリソース、異なる表現
- 段階的な migration が容易
デメリット:
- debug が難しい (header が見えない場合)
- cache 処理が複雑 (Vary header)
- クライアントライブラリが自動で header を追加する必要がある
採用: Stripe (最も有名)、Azure
2.3 Content Negotiation
GET /users/123 HTTP/1.1
Accept: application/vnd.example.user.v2+json
メリット:
- HTTP 標準 (Accept header を活用)
- 同じ URL で異なるバージョン
- HATEOAS とよくマッチ
デメリット:
- 複雑
- クライアントが正確な MIME type を知る必要あり
採用: GitHub (オプショナル、Accept: application/vnd.github.v3+json)
2.4 Query Parameter
GET /users/123?version=2
GET /users/123?api_version=2024-04-15
メリット:
- 最もシンプル
- URL に現れる (debug しやすい)
デメリット:
- 「データの一部」に見える (実際はメタデータ)
- cache 処理が複雑
採用: 一部のシンプルな API
2.5 比較表
| 方式 | URL がきれい | debug | cache | 採用 |
|---|---|---|---|---|
| URL Path | NG | 高 | 高 | GitHub、Twitter |
| Header | 高 | 低 | 中 | Stripe、Azure |
| Content Negotiation | 高 | 中 | 高 | GitHub (オプション) |
| Query Param | 中 | 高 | 中 | シンプル API |
3. Stripe の天才的な日付ベース Versioning
3.1 コアアイデア
Stripe はバージョンを 日付 で表現します:
2024-04-15(その日の API 動作)2023-10-16(以前の動作)2020-08-27(5 年前の動作)
すべての変更は日付で識別されます。
3.2 クライアント利用
import stripe
# アカウントのデフォルトバージョンを使用
stripe.api_version = "2024-04-15"
# または request ごと
stripe.Charge.create(
amount=2000,
currency="usd",
api_version="2023-10-16" # 古い動作
)
3.3 Stripe の秘訣 — 変換レイヤー
server にはただ 1 つのコードベース (最新バージョン)。
各 request に対して:
- クライアントバージョンを確認
- request を最新バージョンに変換 (forward transform)
- 処理
- response をクライアントバージョンに変換 (backward transform)
Client (v2020) → [変換] → 最新コード → [変換] → Client (v2020)
効果:
- 5 年前のクライアントも動作
- コードは最新状態を維持
- 新機能は即座にすべてのユーザーへ (opt-in)
3.4 変換の例
v2020 から v2024 への変更: response に description field 追加。
# 変換関数
def transform_to_v2020(response):
if "description" in response:
del response["description"] # v2020 クライアントはこの field を知らない
return response
v2020 から v2024 への変更: amount が整数からオブジェクトに変更。
def transform_to_v2020(response):
if isinstance(response.get("amount"), dict):
response["amount"] = response["amount"]["value"]
return response
3.5 Stripe の changelog
各バージョン変更が正確にドキュメント化されます:
2024-04-15
- Added
descriptionfield toChargeobjectamountfield type changed from integer to AmountObject- Default
currencyis now derived from account settingsMigration guide: ...
このレベルの透明性が信頼の核心です。
4. GraphQL のバージョンレス進化
4.1 コア哲学
GraphQL は バージョンを使いません。代わりに field 単位で進化:
- 追加: 新しい field を追加 — 既存のクライアントは知らないので影響なし
- 削除:
@deprecatedでマークした後、一定期間後に削除
type User {
id: ID!
name: String!
# Deprecated field
email: String @deprecated(reason: "Use 'emailAddress' instead. Will be removed 2025-12-31")
emailAddress: String!
}
4.2 クライアントが正確にリクエスト
query {
user(id: "123") {
id
name
emailAddress # 新しい field のみリクエスト
}
}
既存クライアントは email をリクエストし続ける → 動作。新しいクライアントは emailAddress を使用。
Over-fetching がない = 新しい field の追加が無料。
4.3 Deprecation のトラッキング
GraphQL server が使用統計を収集:
- どのクライアントが
emailを使っているか? - 最後の使用はいつ?
- 安全に削除可能か?
Apollo Studio、Hasura Cloud がこの機能を提供。
4.4 GraphQL の限界
- すべての変更が互換とは限らない — type 変更、enum 値削除などは依然として breaking
- クライアントコード生成 — 新しい schema で再 build が必要
- 高度なツールが必要 — 使用トラッキングなど
採用: GitHub、Shopify、Twitter、Airbnb
5. Semantic Versioning と API
5.1 SemVer の基本
MAJOR.MINOR.PATCH
v1.2.3
- MAJOR: 互換性が壊れる (breaking)
- MINOR: 互換維持 + 新機能
- PATCH: 互換維持 + bug fix
5.2 ライブラリ vs API
ライブラリ: SemVer が自然。
npm install foo@^1.0.0→ 1.x.x が自動 update
Web API: 適用が難しい。
- クライアントが自動で update できない
- 「v1.2」と「v1.3」の差は意味があるが、「v1.2.3」と「v1.2.4」はほぼ意味がない
現実: Web API は通常 major version のみ表示 (/v1、/v2)。
5.3 SemVer の限界
v2.0.0 が出るとすべてのユーザーが migration 必要。段階的な進化が難しい。
→ Stripe 方式 (日付ベース) または GraphQL 方式 (バージョンなし) がよりエレガント。
6. Deprecation と Sunset
6.1 Deprecation ステージ
- Announce: blog、email、changelog
- Mark in API: response header または field
- Monitor: 使用トラッキング
- Reminder: 利用クライアントに直接通知
- Sunset: 廃止 (HTTP 410 Gone)
6.2 Deprecation header
RFC 8594: HTTP Sunset header
HTTP/1.1 200 OK
Sunset: Sat, 31 Dec 2025 23:59:59 GMT
Deprecation: Sat, 31 Dec 2024 23:59:59 GMT
Link: <https://api.example.com/docs/migration>; rel="deprecation"
意味:
Deprecation: すでに deprecated (まだ動作)Sunset: 廃止スケジュールLink: migration guide
6.3 Deprecation メッセージ (response body)
{
"data": {...},
"warnings": [
{
"code": "DEPRECATED_FIELD",
"message": "Field 'email' is deprecated. Use 'emailAddress' instead.",
"documentation_url": "https://api.example.com/docs/v2#email-deprecation",
"sunset_date": "2025-12-31"
}
]
}
6.4 ユーザーへの通知
技術的:
- response header (
Sunset、Deprecation) - response body の warnings field
コミュニケーション:
- email (登録済み開発者)
- blog / changelog
- ダッシュボード告知
- 直接連絡 (大口ユーザー)
Stripe: 使用中の deprecated API について 自動 email を送信。
6.5 Sunset ポリシーの例
| ユーザー | Sunset 期間 |
|---|---|
| 無料ユーザー | 6 ヶ月 |
| 有料ユーザー | 1 年 |
| エンタープライズ | 2 年 |
大きな企業ほど変更に時間が必要。
7. Breaking Changes 回避戦略
7.1 Additive Changes を優先
誤った変更:
- "user_email"
+ "email"
正しい変更:
+ "email" // 新しい field を追加
"user_email" // 古い field を維持 (deprecated)
両方の field を一緒に返す。クライアントに migration する時間を与える。
7.2 新しい endpoint vs 既存の変更
Bad: 既存の /users の response 形式を変更。
Good: 新しい /v2/users endpoint、または /users?format=new。
7.3 デフォルト値に注意
// v1
{ "page_size": 20 } // デフォルト 20
// v2 — 50 に変更?
{ "page_size": 50 } // Breaking! (pagination 動作が変わる)
デフォルト値の変更はしばしば breaking です。
7.4 Optional → Required は Breaking
- email: string? // optional
+ email: string // required
既存のクライアントが email を送らないと失敗。追加しないでください。
7.5 Enum 値の追加は安全?
enum Status {
ACTIVE,
INACTIVE,
+ PENDING_REVIEW // 新しい値を追加
}
微妙: クライアントが enum を switch で処理していると、新しい値の default ケースが必要。あれば安全、なければ subtle bug。
アドバイス: enum の追加は技術的には backward compatible だが、クライアントコードのレビューが必要。
8. 実際の API 進化事例
8.1 Stripe — 日付ベースのエレガンス
- 10 年以上の API 進化
- すべての変更に正確な日付
- クライアントは自分のペースで upgrade
- 使用統計 + 自動通知
8.2 GitHub — REST から GraphQL へ
- REST v3: 2014 年から運用
- GraphQL v4: 2017 年リリース
- 2 つの API を並行運用
- 新機能は GraphQL 優先
教訓: 既存 API はそのまま残し、新しい paradigm は別途スタート。
8.3 Twilio — Major version + 段階的 migration
/2008-08-01/、/2010-04-01/のような日付 prefix- しかし major change は新しい prefix
- 古いバージョンは数年間維持
8.4 Slack — 段階的 deprecation
- 頻繁に新しいメソッドを追加
- Deprecation は 6 ヶ月〜1 年前に告知
- ユーザーに直接 email
8.5 AWS — ほぼ絶対に壊さない
- 2006 年にリリースされた S3 API が いまだに動作
- 新機能は追加のみ、既存は絶対に変更しない
- 結果: API は一貫性がなく複雑だが、互換性は完璧
9. ベストプラクティス チェックリスト
9.1 設計段階
- Versioning 戦略を決定 (URL/Header/日付)
- 明確な SLA (何年サポートするか?)
- Deprecation ポリシーをドキュメント化
- changelog の自動化
9.2 変更時
- Breaking change か? (チェックリストで確認)
- Additive にできるか?
- Migration guide を作成
- Sunset header を追加
- ユーザーに告知
- 使用統計を monitoring
9.3 Sunset 時
- ユーザー 0 まで待つ
- 最後の通知
- HTTP 410 Gone で応答
- コードを削除 (時間経過後)
10. API 進化の未来
10.1 OpenAPI 3.1 + JSON Schema
schema による自動互換性検査:
- API spec 変更時に自動で breaking change を検知
- クライアントコードの自動生成
10.2 AI ベースの migration
- AI がコードを分析 → 自動 migration PR を生成
- 変更の影響度を自動分析
10.3 contract testing の標準化
- Pact、Spring Cloud Contract のようなツール
- API 提供者とコンシューマー間の契約を強制
- CI で互換性を検証
クイズ
1. 最も一般的な Breaking Change は?
答え: response field の削除または rename です。クライアントがその field を使用中なら即座に壊れます。安全な代替案: 新しい field を追加し、既存の field を deprecated としてマーク (両方を返す)。一定期間後に削除。その他よくある breaking changes: field type の変更 (string → object)、必須 field の追加、デフォルト値の変更、enum 値の意味変更。
2. Stripe の日付ベース versioning の利点は?
答え: (1) 段階的 migration — クライアントが自分のペースで新しいバージョンを採用、(2) 単一のコードベース — server は最新バージョンのみ維持し、変換レイヤーで古いクライアントをサポート、(3) 明確な changelog — 各日付の変更事項が正確にドキュメント化、(4) テスト容易 — 特定の日付バージョンを明示的にテスト可能。デメリットは変換レイヤーの実装が複雑なこと。
3. GraphQL がバージョンを使わない理由は?
答え: GraphQL は クライアントが正確に必要な field のみをリクエスト するため、新しい field の追加が既存クライアントに影響しません — 彼らはその field をリクエストしないので。Field の削除は @deprecated でマークし、使用統計をトラッキングして安全に削除可能。結果: バージョンなしで永遠に進化する API。デメリットはすべての変更が互換とは限らないこと (type 変更、enum 値削除など)。
4. Sunset header の役割は?
答え: RFC 8594 標準 HTTP header で、「このリソースがいつ廃止されるか」を伝えます。Sunset: Sat, 31 Dec 2025 23:59:59 GMT。クライアントはこの header を見て、migration スケジュールを自動認識できます。Deprecation header と Link header (migration guide) と共に使用。自動化されたクライアントが廃止スケジュールに安全に対応できるようにします。
5. Hyrum's Law が API 設計に意味することは?
答え: 「API に十分なユーザーがいれば、どの観測可能な動作も誰かが依存している。」つまり、公式ドキュメントになくても変更してはいけません。response field の順序、error message の正確なテキスト、response time、ID の連番性などすべてが「API の一部」になります。結論: (1) 最初から慎重に設計、(2) 変更時はすべての観測可能な動作を考慮、(3) 意図しない動作は明示的に「この動作に依存しないでください」とドキュメント化。
参考資料
- Stripe API Versioning — 日付ベースアプローチの解説
- GitHub API v3 to v4 — GraphQL migration
- RFC 8594 — Sunset Header
- Hyrum's Law
- OpenAPI 3.1 — API 仕様標準
- Pact — Contract testing
- GraphQL Best Practices
- API Stylebook — 企業別 API guide 集
- Twilio API Versioning
- Building Evolvable Web APIs — Glenn Block et al.
- API Change Management at Stripe
현재 단락 (1/284)
- **Versioning 戦略 4 つ**: URL Path (`/v1/`)、Header (`API-Version: 2024-01-01`)、Content Negotiation (`...