バージョニング戦略 — 壊さずに進化させる
破壊的変更の恐怖
「サクラさん、大変です」
パートナー企業A社のシステムエラー報告がSlackに届いた。テックブリッジAPIのフィールド名を変更したことで、A社のアプリが動かなくなったのだ。
「username を name に変えただけなのに...」
サクラは青ざめた。パブリックAPIを変更することは、使っている全員に影響する。バージョニング設計なしに本番に出てしまったのは失敗だった。
破壊的変更とは何か
後方互換性のある変更はバージョンを上げなくても安全だ。破壊的変更はバージョンを上げる必要がある。
バージョニング戦略の比較
| 戦略 | 例 | メリット | デメリット |
|---|---|---|---|
| URLパス | /v1/users | 直感的、キャッシュしやすい | URL設計が汚れる |
| クエリパラメーター | /users?version=1 | URLが綺麗 | キャッシュが難しい |
| ヘッダー | API-Version: 1 | URLが綺麗 | 見えにくい、ブラウザ操作困難 |
| コンテンツネゴシエーション | Accept: application/vnd.api+json;version=1 | RESTに忠実 | 複雑すぎる |
Stripeは 2023-10-01 のような日付形式をヘッダーで使う。GitHubはURLパスを使う。
このガイドではURLパスを採用する。最も直感的でキャッシュ効率が良い。
URLパスバージョニングの実装
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :users
resources :articles
end
namespace :v2 do
resources :users
resources :articles
end
end
endディレクトリ構造
app/controllers/
api/
v1/
users_controller.rb
articles_controller.rb
base_controller.rb
v2/
users_controller.rb
articles_controller.rb
base_controller.rb
共通ロジックの継承
# app/controllers/api/base_controller.rb
module Api
class BaseController < ApplicationController
include ApiKeyAuthenticatable
include ApiResponse
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::ParameterMissing, with: :bad_request
private
def not_found
render_error(code: "not_found", message: "リソースが見つかりません", status: :not_found)
end
def bad_request(e)
render_error(code: "bad_request", message: e.message, status: :bad_request)
end
end
end
# app/controllers/api/v1/base_controller.rb
module Api
module V1
class BaseController < Api::BaseController
# V1固有の設定
end
end
end
# app/controllers/api/v2/base_controller.rb
module Api
module V2
class BaseController < Api::BaseController
# V2固有の設定
end
end
endV1からV2への移行
V2では、ユーザーレスポンスの username を name に変更するとする。
# app/controllers/api/v1/users_controller.rb
module Api
module V1
class UsersController < BaseController
def show
render json: {
data: {
id: @user.id,
username: @user.name, # 旧フィールド名
email: @user.email
}
}
end
end
end
end
# app/controllers/api/v2/users_controller.rb
module Api
module V2
class UsersController < BaseController
def show
render json: {
data: {
id: @user.id,
name: @user.name, # 新フィールド名
email: @user.email,
profile: @user.profile # V2で追加
}
}
end
end
end
endシリアライザーのバージョン管理
# app/serializers/v1/user_serializer.rb
module V1
class UserSerializer
include JSONAPI::Serializer
attributes :email
attribute :username do |user|
user.name # 旧フィールド名にマッピング
end
end
end
# app/serializers/v2/user_serializer.rb
module V2
class UserSerializer
include JSONAPI::Serializer
attributes :name, :email, :bio, :created_at
attribute :created_at do |user|
user.created_at.iso8601
end
end
end非推奨(Deprecation)の通知
V1を廃止する前に、十分な猶予期間を設けて警告する。
# app/controllers/api/v1/base_controller.rb
module Api
module V1
class BaseController < Api::BaseController
DEPRECATION_DATE = Date.new(2025, 6, 1)
after_action :add_deprecation_header
private
def add_deprecation_header
response.headers["Sunset"] = DEPRECATION_DATE.httpdate
response.headers["Deprecation"] = "true"
response.headers["Link"] = '<https://docs.techbridge.jp/migration/v2>; rel="successor-version"'
end
end
end
endクライアントには以下のヘッダーが届く:
HTTP/1.1 200 OK
Sunset: Mon, 01 Jun 2025 00:00:00 GMT
Deprecation: true
Link: <https://docs.techbridge.jp/migration/v2>; rel="successor-version"
INFO
Sunset ヘッダーはRFC 8594で定義された標準ヘッダーです。クライアントはこれを読んで廃止日前に移行できます。
バージョン移行ガイドの自動生成
# lib/tasks/api_diff.rake
namespace :api do
desc "V1とV2のエンドポイント差分を出力"
task diff: :environment do
v1_routes = Rails.application.routes.routes
.select { |r| r.path.spec.to_s.include?("/api/v1/") }
v2_routes = Rails.application.routes.routes
.select { |r| r.path.spec.to_s.include?("/api/v2/") }
puts "=== V1にあってV2にないエンドポイント ==="
(v1_routes.map(&:path) - v2_routes.map(&:path)).each do |path|
puts " REMOVED: #{path.spec}"
end
end
endAWS API Gatewayでのバージョン管理
API GatewayはStageでバージョンを管理する。
# CloudFormation
ApiGatewayV1Stage:
Type: AWS::ApiGateway::Stage
Properties:
RestApiId: !Ref ApiGateway
StageName: v1
Variables:
backendUrl: "http://api-v1.techbridge.internal"
ApiGatewayV2Stage:
Type: AWS::ApiGateway::Stage
Properties:
RestApiId: !Ref ApiGateway
StageName: v2
Variables:
backendUrl: "http://api-v2.techbridge.internal"# V1: https://api.techbridge.jp/v1/users
# V2: https://api.techbridge.jp/v2/users
バージョンサポートポリシー
## テックブリッジ API バージョンポリシー
- 各バージョンは最低18ヶ月サポート
- 廃止の6ヶ月前に告知
- 廃止後はSunsetヘッダーで警告→完全停止
- 新バージョンリリース後も旧バージョンを並行稼働# config/api_versions.rb
API_VERSIONS = {
v1: {
release_date: Date.new(2023, 1, 1),
sunset_date: Date.new(2025, 6, 1),
status: :deprecated
},
v2: {
release_date: Date.new(2024, 1, 1),
sunset_date: nil,
status: :current
}
}.freezeテストでバージョンを守る
# spec/requests/api/v1/users_spec.rb
RSpec.describe "API V1 Users" do
it "V1は username フィールドを返す" do
user = create(:user, name: "田中太郎")
get "/api/v1/users/#{user.id}",
headers: { "X-API-Key" => api_key }
expect(response).to have_http_status(:ok)
expect(json_response["data"]["username"]).to eq("田中太郎")
expect(json_response["data"]["name"]).to be_nil # V1にはない
end
end
# spec/requests/api/v2/users_spec.rb
RSpec.describe "API V2 Users" do
it "V2は name フィールドを返す" do
user = create(:user, name: "田中太郎")
get "/api/v2/users/#{user.id}",
headers: { "X-API-Key" => api_key }
expect(response).to have_http_status(:ok)
expect(json_response["data"]["name"]).to eq("田中太郎")
expect(json_response["data"]["username"]).to be_nil # V2にはない
end
endWARNING
バージョンテストはCI/CDパイプラインに必ず含めてください。デプロイのたびに全バージョンのテストを実行し、既存バージョンが壊れていないことを確認します。
サクラの教訓
A社からのエラー報告を受けて、サクラはルールを決めた。
「今後はフィールドを変更する前に必ずバージョンを上げる。そして新バージョンリリース前に移行ガイドとSunsetヘッダーで十分に予告する」
APIは約束だ。一度公開したら、ユーザーはその約束に依存してビジネスを組み立てる。壊すことは裏切りになる。
次章では、大量データを効率的に返す「ページネーションとフィルタリング」を設計する。