ストーリー
高橋アーキテクトが質問した。
なぜバージョニングが必要か
APIの変更がもたらすリスク
// v1のレスポンス
{
"name": "田中太郎" // 単一の名前フィールド
}
// v2で名前を姓・名に分離したい
{
"firstName": "太郎",
"lastName": "田中"
}
// 問題: v1を使っているクライアントは "name" を参照している
// → "name" がなくなると、クライアントが壊れる(破壊的変更)
変更の種類
| 変更の種類 | 後方互換性 | 例 |
|---|---|---|
| フィールド追加 | 互換 | 新しいフィールドの追加 |
| オプショナルパラメータ追加 | 互換 | 新しいクエリパラメータ |
| フィールド削除 | 非互換 | 既存フィールドの削除 |
| フィールド名変更 | 非互換 | name → fullName |
| 型の変更 | 非互換 | integer → string |
| 必須パラメータ追加 | 非互換 | 新しい必須フィールド |
3つのバージョニング戦略
1. URIバージョニング
// URLパスにバージョンを含める(最も一般的)
GET /api/v1/users/123 // バージョン1
GET /api/v2/users/123 // バージョン2
// メリット:
// - 分かりやすい(URLを見ればバージョンが分かる)
// - ブラウザでテストしやすい
// - キャッシュが容易(URLが異なるため)
// - 多くのAPIで採用されている(GitHub, Stripe, Twitter等)
// デメリット:
// - URLが変わるため、すべてのクライアントの変更が必要
// - 同じリソースに複数のURLが存在する
2. ヘッダーバージョニング
// カスタムヘッダーでバージョンを指定
GET /api/users/123
Accept: application/vnd.taskflow.v2+json
// または独自ヘッダー
GET /api/users/123
X-API-Version: 2
// メリット:
// - URLがクリーン(バージョン情報を含まない)
// - コンテンツネゴシエーションの仕組みに沿っている
// デメリット:
// - ブラウザでテストしにくい
// - ドキュメントに明示しにくい
// - クライアント側でヘッダー設定が必要
3. クエリパラメータバージョニング
// クエリパラメータでバージョンを指定
GET /api/users/123?version=2
GET /api/users/123?v=2
// メリット:
// - URLベースで分かりやすい
// - デフォルトバージョンを設定しやすい
// デメリット:
// - クエリパラメータが増える
// - キャッシュキーが複雑になる
// - あまり一般的ではない
戦略の比較
| 戦略 | 明確さ | テスト容易性 | URL変更 | 採用例 |
|---|---|---|---|---|
| URI | 高 | 高 | あり | GitHub, Stripe, Twitter |
| ヘッダー | 中 | 低 | なし | Azure, GitHub (v3) |
| クエリ | 中 | 中 | なし | Google, Amazon |
実装パターン
URIバージョニングの実装
// Express でのルーティング
import { Router } from 'express';
// v1 のルーター
const v1Router = Router();
v1Router.get('/users/:id', async (req, res) => {
const user = await userRepository.findById(req.params.id);
// v1の形式で返す
res.json({
data: {
id: user.id,
name: `${user.lastName} ${user.firstName}`, // 姓名結合
email: user.email,
}
});
});
// v2 のルーター
const v2Router = Router();
v2Router.get('/users/:id', async (req, res) => {
const user = await userRepository.findById(req.params.id);
// v2の形式で返す
res.json({
data: {
id: user.id,
firstName: user.firstName, // 姓名分離
lastName: user.lastName,
email: user.email,
avatarUrl: user.avatarUrl, // v2で追加
}
});
});
// マウント
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
ヘッダーバージョニングの実装
// ミドルウェアでバージョンを判定
function versionMiddleware(req: Request, res: Response, next: NextFunction) {
const acceptHeader = req.headers.accept || '';
const versionMatch = acceptHeader.match(/vnd\.taskflow\.v(\d+)/);
req.apiVersion = versionMatch ? parseInt(versionMatch[1]) : 1; // デフォルトv1
next();
}
app.use(versionMiddleware);
app.get('/api/users/:id', async (req, res) => {
const user = await userRepository.findById(req.params.id);
if (req.apiVersion === 2) {
res.json({ data: { firstName: user.firstName, lastName: user.lastName } });
} else {
res.json({ data: { name: `${user.lastName} ${user.firstName}` } });
}
});
どの戦略を選ぶべきか
公開API(サードパーティ向け)?
├── Yes → URIバージョニング(最も分かりやすい)
└── No → 内部API?
├── Yes → ヘッダーバージョニング or バージョニングなし
└── GraphQL?
└── Yes → バージョニングなし(@deprecated で段階的移行)
まとめ
| 戦略 | 推奨場面 | 特徴 |
|---|---|---|
| URIバージョニング | 公開API | 最も分かりやすく、一般的 |
| ヘッダーバージョニング | 内部API | URLがクリーン |
| クエリパラメータ | 特殊なケース | デフォルトバージョンの設定が容易 |
| バージョニングなし | GraphQL | @deprecated で段階的移行 |
チェックリスト
- バージョニングが必要な理由(破壊的変更)を説明できる
- 3つのバージョニング戦略の違いを理解した
- 場面に応じた戦略の選択ができる
- バージョニングの実装パターンを把握した
次のステップへ
バージョニング戦略を学びました。
次のセクションでは、後方互換性の維持について詳しく学びます。 バージョンを上げなくても済む設計テクニックを身につけましょう。
推定読了時間: 30分