Webシステムやマイクロサービスアーキテクチャの開発において、APIの仕様変更は避けて通れません。しかし、不用意な変更はクライアントアプリケーションのクラッシュや、連携システムの動作不良といった障害を引き起こします。
この記事では、APIの仕様変更に伴う障害を防ぐための「バージョニング手法の選定基準」「後方互換性を維持するための設計ルール」「具体的な実装例」、および「リリース時のチェックリスト」を解説します。
対象読者
- Web API(REST API)の設計・開発に携わるエンジニア
- マイクロサービス間の通信仕様の管理に課題を感じている開発者
- APIのバージョン移行を安全に進めたいプロジェクトリーダー
1. APIバージョニング手法の比較と選定基準
APIのバージョンを管理する代表的な3つの手法について、それぞれの特徴とメリット・デメリットを整理します。プロジェクトの要件やクライアントの特性に合わせて適切な手法を選択してください。
| 手法 | 実装例 | メリット | デメリット | 主なユースケース |
|---|---|---|---|---|
| URIパス | /api/v1/users |
・ルーティングが容易 ・ブラウザやプロキシでのキャッシュが効きやすい |
・URIがリソースの場所ではなくバージョンに依存する | 一般公開API、サードパーティ連携 |
| カスタムヘッダー | X-API-Version: 1.1 |
・URIをクリーンに保てる ・同一URIで複数バージョンを切り替え可能 |
・ブラウザからの直接アクセスやキャッシュ制御が複雑化する | 社内マイクロサービス間通信、SPA/モバイルアプリ専用API |
| Acceptヘッダー (Media Type) | Accept: application/vnd.company.v1+json |
・RESTの原則(コンテンツネゴシエーション)に準拠 | ・クライアント側のリクエストヘッダー構成が煩雑になる | 厳格なREST API設計を求めるエンタープライズシステム |
選定の判断基準
- 一般公開APIやサードパーティ向けAPI:直感的でクライアント側の実装負荷が低いURIパス方式を推奨します。
- 社内システム・マイクロサービス間:URIの変更を最小限に抑え、柔軟なルーティング制御を行うためにカスタムヘッダー方式またはURIパス方式を検討します。
2. 後方互換性を破壊する変更と維持する変更
APIの変更を行う際、何が「互換性を壊す変更(破壊的変更)」であり、何が「互換性を維持する変更」であるかを開発チーム内で共通認識化しておくことが重要です。
互換性を維持する変更(Non-Breaking Changes)
- 新しいAPIエンドポイントの追加
- レスポンスへの新しいフィールドの追加(※クライアント側が未知のフィールドを無視する実装になっていることが前提)
- リクエストにおける任意(Optional)パラメータの追加
後方互換性を破壊する変更(Breaking Changes)
- 既存のフィールドの削除、またはフィールド名の変更
- フィールドのデータ型の変更(例:
intからstringへの変更) - 必須(Required)パラメータの追加
- エラーレスポンスのステータスコードや構造の変更
3. Go言語による後方互換性を意識したAPI実装例
ここでは、Go言語を用いたWeb APIにおいて、レスポンスの互換性を維持しつつ新しいバージョンに対応する実装例を示します。
シナリオ
初期バージョン(v1)ではユーザー名を name という1つのフィールドで返していましたが、新バージョン(v2)では first_name と last_name に分割することになりました。既存のv1クライアントを壊さないよう、同一のデータソースから両方のバージョンに対応します。
package main
import (
"encoding/json"
"net/http"
"strings"
)
// User 内部のドメインモデル
type User struct {
ID string
FirstName string
LastName string
}
// UserResponseV1 旧バージョン(v1)用のレスポンス構造体
type UserResponseV1 struct {
ID string `json:"id"`
Name string `json:"name"` // フルネームを返す
}
// UserResponseV2 新バージョン(v2)用のレスポンス構造体
type UserResponseV2 struct {
ID string `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
// 疑似データベースからユーザーを取得する関数
func fetchUser() User {
return User{
ID: "usr_12345",
FirstName: "Taro",
LastName: "Yamada",
}
}
// v1ハンドラー
func userHandlerV1(w http.ResponseWriter, r *http.Request) {
u := fetchUser()
resp := UserResponseV1{
ID: u.ID,
Name: u.FirstName + " " + u.LastName,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
// v2ハンドラー
func userHandlerV2(w http.ResponseWriter, r *http.Request) {
u := fetchUser()
resp := UserResponseV2{
ID: u.ID,
FirstName: u.FirstName,
LastName: u.LastName,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func main() {
// URIパスによるルーティングの分離
http.HandleFunc("/api/v1/user", userHandlerV1)
http.HandleFunc("/api/v2/user", userHandlerV2)
// サーバー起動(ポート8080)
// http.ListenAndServe(":8080", nil)
}
※上記のコードは構造を理解するための簡易的な例です。実際のプロダクション環境に導入する際は、適切なエラーハンドリングやミドルウェアの設定を行ってください。
4. API仕様変更時のリリースチェックリスト
APIの仕様変更やバージョンアップを行う際、本番障害を防ぐために以下のチェックリストを用いて確認を行います。
-
クライアント実装の確認
- クライアント側(SPA、モバイルアプリ、他マイクロサービス)が、レスポンス内の「未知のフィールド」を自動的に無視する設計(堅牢性の原則)になっているか。
-
後方互換性の検証
- 既存のフィールドの削除、リネーム、データ型の変更を行っていないか。
- 新規追加したパラメータはすべて「任意(Optional)」になっているか。必須にする場合、デフォルト値が定義されているか。
-
ルーティングとフォールバック
- バージョン指定がないリクエストが来た場合、どのデフォルトバージョンにルーティングされるか明確になっているか。
- 非推奨(Deprecated)にするバージョンについて、移行期間と廃止スケジュールがクライアント側に通知されているか。
-
監視とログ
- バージョンごとのリクエスト数やエラーレートを監視できるダッシュボードが用意されているか。
5. 導入時の注意点とよくある失敗
失敗例:クライアント側のパースエラー
APIレスポンスに新しいフィールドを追加した際、クライアントアプリ(特にモバイルアプリや古いライブラリ)がJSONのデシリアライズ時に「定義されていないフィールドが存在する」としてエラーを吐き、クラッシュするケースがあります。
対策:
APIを設計する初期段階から、クライアント側で「未知のプロパティを許容して無視する」デシリアライズ設定(例:Java/Jacksonにおける FAIL_ON_UNKNOWN_PROPERTIES = false など)を徹底させておく必要があります。これが担保できない場合は、マイナーアップデートであっても新バージョン(v2など)としてエンドポイントを分離することを検討してください。
まとめ
APIの仕様変更による障害を防ぐためには、以下の3点が重要です。
- システムの特性に合ったバージョニング方式(URIパス、ヘッダーなど)を早期に決定すること。
- 後方互換性を壊す変更を厳格に定義し、既存クライアントに影響を与える変更は必ず新バージョンとして提供すること。
- クライアント側での堅牢なパース処理を共通の設計ルールとして設けておくこと。
APIは一度公開すると変更のコントロールが難しくなります。開発の初期段階から変更を前提とした設計を取り入れ、安全なシステム運用を実現しましょう。