混乱を招かないAPIの進化
REST APIを構築すること自体はそれほど難しくありません。本当の試練は、そのAPIを利用している2万以上のモバイルアプリやサードパーティ製連携を壊すことなく、APIの挙動を変更しなければならない時に訪ります。たとえば、JSONのフィールド名をuser_idからuuidに変更しただけで、すべてのレガシークライアントは即座に動作しなくなります。バージョニングは、こうした破壊的変更に対する保険なのです。
効果的なバージョニングを行うことで、古いクライアントの機能を維持したまま、新機能やモダンなデータ構造を提供できるようになります。本番環境のバックエンドを管理してきた私の経験では、明確なバージョニング戦略によって移行時の摩擦が約40%軽減され、エンジニアリングチームが緊急のホットフィックスに追われることもなくなりました。これにより、予測可能な成長パスが構築されます。
クイックスタート:URIバージョニング
URIバージョニングは、最も抵抗の少ない方法です。単にバージョン番号(v1、v2)をURLパスに直接組み込むだけです。明示的で、ログの検索(grep)もしやすく、標準的なブラウザキャッシュとの相性も抜群です。
Node.js (Express) での実装
Expressでは、ルーターがこの目的に最適なツールです。コード内に物理的な境界を作ることで、v2のロジックが安定したv1のエンドポイントに誤って混入するのを防ぐことができます。
const express = require('express');
const app = express();
// V1: レガシーなデータ構造
const v1Router = express.Router();
v1Router.get('/user', (req, res) => {
res.json({ id: 101, name: "アリス・スミス" });
});
// V2: モダンで正規化された構造
const v2Router = express.Router();
v2Router.get('/user', (req, res) => {
res.json({
id: "550e8400-e29b-41d4-a716-446655440000",
first_name: "アリス",
last_name: "スミス"
});
});
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
app.listen(3000);
FastAPI での実装
FastAPIでは、APIRouterクラスを使用してこれを処理します。ここでの大きな利点は、FastAPIがバージョンごとに個別のSwaggerドキュメントを自動生成できることです。これにより、フロントエンドチームは何が変更されたかを正確に把握しやすくなります。
from fastapi import FastAPI, APIRouter
app = FastAPI()
# V1: シンプルなアイテムリスト
v1 = APIRouter(prefix="/v1", tags=["v1"])
@v1.get("/items")
def get_items_v1():
return [{"name": "ノートパソコン", "price": 1200}]
# V2: メタデータの拡張と通貨対応
v2 = APIRouter(prefix="/v2", tags=["v2"])
@v2.get("/items")
def get_items_v2():
return [{
"id": "prod_01",
"name": "ノートパソコン",
"amount": 1200,
"currency": "USD",
"in_stock": True
}]
app.include_router(v1)
app.include_router(v2)
適切な戦略の選択
URIバージョニングが業界標準となっているのには理由があります。それは「可視性」です。しかし、特定のアーキテクチャ上のニーズによっては、他の手法が適している場合もあります。
1. URIバージョニング (/v1/resource)
これは公開APIで最も一般的な選択肢です。透過的であり、どのブラウザでも簡単にテストできます。
- メリット: 可視性が高い、実装がシンプル、CDNとの相性が非常に良い。
- デメリット: バージョンの変更が同じデータに対して「新しい」リソースを作成することになるため、厳密にはRESTの原則に反する。
2. ヘッダーバージョニング (X-API-Version: 2)
クライアントはカスタムHTTPヘッダーでバージョンを指定します。これによりURLがクリーンに保たれ、リソースそのものに集中できます。
GET /api/user
Host: api.example.com
X-API-Version: 2
Node.jsでは、req.headers['x-api-version']をチェックするミドルウェアを通じてこれを処理できます。見た目はスッキリしますが、URLを共有するだけで特定のバージョンの挙動を再現することができないため、デバッグが難しくなります。
3. メディアタイプ (Acceptヘッダー)
これは「純粋な」RESTのアプローチです。クライアントはコンテンツネゴシエーションを通じて特定のスキーマを要求します。
Accept: application/vnd.myapi.v2+json
エレガントではありますが、異なるクライアントタイプ(Web、モバイル、IoT)で正しく実装するのが非常に難しいことで知られています。また、同じURLでもヘッダーによって異なるデータ構造を返すため、キャッシュ処理が複雑になります。
非推奨ライフサイクルの管理
v2をリリースしたからといって、すぐにv1を削除できるわけではありません。プロフェッショナルなAPIには、クライアントが安全に移行できるように、通常6ヶ月から12ヶ月程度の猶予期間を設ける非推奨ポリシーが必要です。
何が破壊的変更に該当するか?
クライアントがコードを書き直さなければならない場合、それは破壊的変更です。一般的な例を以下に挙げます:
- JSONフィールドの名前変更または削除
- IDをInteger(整数)からUUIDに変更
- 以前サポートされていたクエリパラメータの削除
404 Not Foundから410 Goneへの変更
Sunsetヘッダー
開発者に配慮しましょう。Sunset HTTPヘッダー(RFC 8594)を使用して、エンドポイントが完全に廃止される正確な日時を開発者に伝えます。
HTTP/1.1 200 OK
Deprecation: true
Sunset: Thu, 31 Dec 2026 23:59:59 GMT
Content-Type: application/json
このヘッダーにより、クライアントは実際に削除が行われる前に、今後の廃止予定をプログラムで検知してログに記録できるようになります。
長期的なメンテナンスのためのアーキテクチャ
コードをコピペしてはいけません。ロジックの80%がバージョン間で共有されている場合は、そのロジックをサービスレイヤーに移動しましょう。コントローラー(v1とv2)は、共通のサービス出力をクライアントが必要とするバージョン固有のJSONスキーマに変換する「トランスレーター」としてのみ機能させるべきです。
実行チェックリスト
これら4つのルールを守ることで、メンテナンスにかかる膨大な時間を節約できます。
- メジャーバージョンに固定する: URLには
v1やv2を使用します。1.2.3のようなセマンティックバージョニングは、内部パッケージやタグ用にとっておきましょう。 - ドキュメントを自動化する:
swagger-ui-expressやFastAPIの組み込みドキュメント機能を使用します。古くなったドキュメントは、ドキュメントがないよりも質が悪いです。 - クロスバージョンテスト: データベースクエリを最適化する際は、アクティブな「すべて」のバージョンに対してテストスイートを実行してください。v2を高速化しようとしてv1を壊してしまうのは非常によくあることです。
- リダイレクトを活用する: バージョン間でリソースに変更がない場合は、v2のルートから単にv1のハンドラーを呼び出すようにして、重複を最小限に抑えます。
バージョニングは、プロフェッショナルなバックエンドエンジニアリングの証です。初日から変更を計画しておくことで、既存のユーザーベースを置き去りにすることなく、イノベーションを続けられる俊敏なシステムを維持できます。

