Skip to content
Published on

API Versioning & 進化戦略 完全ガイド 2025: Breaking Changes なしの API 進化、Deprecation、Sunset

Authors

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 の削除BreakingNG
field の renameBreakingNG
field type の変更BreakingNG
必須 field の追加BreakingNG
error code の意味変更BreakingNG
デフォルト動作の変更BreakingNG

ルール: 追加は安全、削除/変更は危険

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 がきれいdebugcache採用
URL PathNGGitHub、Twitter
HeaderStripe、Azure
Content NegotiationGitHub (オプション)
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 に対して:

  1. クライアントバージョンを確認
  2. request を最新バージョンに変換 (forward transform)
  3. 処理
  4. 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 description field to Charge object
  • amount field type changed from integer to AmountObject
  • Default currency is now derived from account settings

Migration 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 StudioHasura 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 ステージ

  1. Announce: blog、email、changelog
  2. Mark in API: response header または field
  3. Monitor: 使用トラッキング
  4. Reminder: 利用クライアントに直接通知
  5. 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 (SunsetDeprecation)
  • 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 の標準化

  • PactSpring 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) 意図しない動作は明示的に「この動作に依存しないでください」とドキュメント化。


参考資料