LESSON 30分

ストーリー

高橋アーキテクト
バージョンを上げるのは最後の手段だ

高橋アーキテクトが力説した。

高橋アーキテクト
バージョンを上げるたびに、古いバージョンのメンテナンスコストが増える。v1 と v2 を同時にサポートするのは、コードベースが2倍になるようなものだ
あなた
じゃあ、どうすれば…
高橋アーキテクト
後方互換性を維持しながら進化する設計テクニックがある。これを覚えれば、バージョンアップの回数を大幅に減らせる

後方互換性の原則

安全な変更(後方互換)

// 1. フィールドの追加(既存クライアントは無視する)
// Before
{ "id": "123", "name": "田中" }

// After(avatarUrl を追加)
{ "id": "123", "name": "田中", "avatarUrl": "https://..." }
// → 既存クライアントは avatarUrl を知らなくても動作する

// 2. オプショナルパラメータの追加
// Before: GET /api/v1/users?page=1
// After:  GET /api/v1/users?page=1&role=admin  (role は任意)
// → role を指定しなくても動作する

// 3. 新しいエンドポイントの追加
// POST /api/v1/users/bulk  (一括作成のエンドポイント追加)
// → 既存のエンドポイントに影響なし

// 4. 新しいHTTPステータスコードの追加
// 429 Too Many Requests を返すようにした
// → クライアントは未知のステータスコードでも4xxとして処理可能

危険な変更(破壊的変更)

// 1. フィールドの削除
// Before: { "id": "123", "name": "田中", "age": 25 }
// After:  { "id": "123", "name": "田中" }
// → "age" を参照しているクライアントが壊れる

// 2. フィールド名の変更
// Before: { "name": "田中" }
// After:  { "fullName": "田中" }
// → "name" を参照しているクライアントが壊れる

// 3. 型の変更
// Before: { "price": 1500 }      // number
// After:  { "price": "1500" }     // string
// → 型が変わるとパースに失敗する

// 4. 必須パラメータの追加
// Before: POST /api/v1/users { "name": "田中" }
// After:  POST /api/v1/users { "name": "田中", "email": "..." }  // emailが必須に
// → email を送らないクライアントがエラーになる

// 5. レスポンス構造の変更
// Before: { "users": [...] }
// After:  { "data": { "users": [...] } }
// → ネスト構造が変わるとパースに失敗する

互換性維持テクニック

テクニック1: フィールドの追加で対応

// 名前を姓・名に分離したい場合

// 悪い例: フィールド名を変更
{ "name": "田中太郎" }           // v1
{ "firstName": "太郎", "lastName": "田中" }  // v2(破壊的変更)

// 良い例: 新フィールドを追加、旧フィールドも残す
{
  "name": "田中太郎",          // 既存(維持)
  "firstName": "太郎",         // 新規追加
  "lastName": "田中"           // 新規追加
}
// → 既存クライアントは "name" を使い続けられる
// → 新しいクライアントは firstName/lastName を使う

テクニック2: デフォルト値の活用

// 新しいパラメータを追加する場合はデフォルト値を設定

// Before
GET /api/v1/users?page=1&perPage=20

// After(sort パラメータ追加)
GET /api/v1/users?page=1&perPage=20&sort=-createdAt
// sort のデフォルト値: "-createdAt"
// → sort を指定しないクライアントでも従来通り動作

// TypeScript の型定義
interface UserListQuery {
  page?: number;         // デフォルト: 1
  perPage?: number;      // デフォルト: 20
  sort?: string;         // デフォルト: "-createdAt"(新規追加)
  role?: string;         // デフォルト: なし(新規追加)
}

テクニック3: Tolerant Reader パターン

// クライアント側の設計: 未知のフィールドを無視する

// 悪い例: レスポンスの全フィールドに厳密な型チェック
interface User {
  id: string;
  name: string;
  email: string;
  // → サーバーが新フィールドを追加するとパースエラーの可能性
}

// 良い例: 必要なフィールドだけ参照し、未知のフィールドは無視
function parseUser(data: Record<string, unknown>): UserView {
  return {
    id: data.id as string,
    name: data.name as string,
    // email は使わないなら参照しない
    // 未知のフィールドがあっても問題ない
  };
}

// Zodを使った実装
import { z } from 'zod';

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
}).passthrough();  // 未知のフィールドを許容

// → サーバーが新しいフィールドを追加してもパースが成功する

テクニック4: Expand/Contract パターン

// 3段階で安全にフィールドを変更する

// Phase 1: Expand(拡張)
// 新しいフィールドを追加、古いフィールドも残す
{
  "name": "田中太郎",       // 旧フィールド
  "firstName": "太郎",      // 新フィールド
  "lastName": "田中"        // 新フィールド
}

// Phase 2: Migrate(移行)
// クライアントに新フィールドへの移行を促す
// ログで旧フィールドの使用状況を監視

// Phase 3: Contract(縮小)
// 十分な移行期間後、旧フィールドを削除
{
  "firstName": "太郎",
  "lastName": "田中"
}

互換性テストの自動化

// コントラクトテスト: APIレスポンスの構造を検証
import { describe, it, expect } from 'vitest';

describe('User API Backward Compatibility', () => {
  it('v1のレスポンス形式が維持されている', async () => {
    const response = await fetch('/api/v1/users/123');
    const body = await response.json();

    // 必須フィールドが存在することを確認
    expect(body.data).toHaveProperty('id');
    expect(body.data).toHaveProperty('name');
    expect(body.data).toHaveProperty('email');

    // 型のチェック
    expect(typeof body.data.id).toBe('string');
    expect(typeof body.data.name).toBe('string');

    // 新しいフィールドが追加されていても OK
    // (テストは既存フィールドの存在のみを確認)
  });
});

まとめ

テクニック説明効果
フィールド追加旧フィールドを残しつつ新フィールドを追加バージョンアップ不要
デフォルト値新パラメータにデフォルト値を設定既存クライアントへの影響なし
Tolerant Readerクライアントが未知のフィールドを無視柔軟な拡張が可能
Expand/Contract3段階で安全にフィールドを移行計画的な変更

チェックリスト

  • 安全な変更と破壊的変更の区別ができる
  • フィールド追加による互換性維持の方法を理解した
  • Tolerant Reader パターンの概念を把握した
  • Expand/Contract パターンの3段階を理解した

次のステップへ

後方互換性の維持テクニックを学びました。

次のセクションでは、API廃止とマイグレーション戦略を学びます。 古いバージョンをどう廃止し、クライアントをどう移行させるかの戦略です。


推定読了時間: 30分