mybook

バージョニング戦略 — 壊さずに進化させる

破壊的変更の恐怖

「サクラさん、大変です」

パートナー企業A社のシステムエラー報告がSlackに届いた。テックブリッジAPIのフィールド名を変更したことで、A社のアプリが動かなくなったのだ。

usernamename に変えただけなのに...」

サクラは青ざめた。パブリックAPIを変更することは、使っている全員に影響する。バージョニング設計なしに本番に出てしまったのは失敗だった。

破壊的変更とは何か

Loading diagram...

後方互換性のある変更はバージョンを上げなくても安全だ。破壊的変更はバージョンを上げる必要がある。

バージョニング戦略の比較

戦略メリットデメリット
URLパス/v1/users直感的、キャッシュしやすいURL設計が汚れる
クエリパラメーター/users?version=1URLが綺麗キャッシュが難しい
ヘッダーAPI-Version: 1URLが綺麗見えにくい、ブラウザ操作困難
コンテンツネゴシエーションAccept: application/vnd.api+json;version=1RESTに忠実複雑すぎる

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
end

V1からV2への移行

V2では、ユーザーレスポンスの usernamename に変更するとする。

# 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
end

AWS 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
end

WARNING

バージョンテストはCI/CDパイプラインに必ず含めてください。デプロイのたびに全バージョンのテストを実行し、既存バージョンが壊れていないことを確認します。

サクラの教訓

A社からのエラー報告を受けて、サクラはルールを決めた。

「今後はフィールドを変更する前に必ずバージョンを上げる。そして新バージョンリリース前に移行ガイドとSunsetヘッダーで十分に予告する」

APIは約束だ。一度公開したら、ユーザーはその約束に依存してビジネスを組み立てる。壊すことは裏切りになる。

次章では、大量データを効率的に返す「ページネーションとフィルタリング」を設計する。