EXERCISE 90分

ストーリー

高橋アーキテクト
TaskFlow API v1 が半年前にリリースされた。いくつかの設計上の問題が見つかり、v2へのアップグレードが決まった

高橋アーキテクトが v1 の問題点リストを渡した。

高橋アーキテクト
v2への移行計画を作ってほしい。バージョニング戦略、互換性の維持、廃止スケジュール、マイグレーションガイド、BFFの検討 — すべて含めた計画だ

ミッション概要

ミッションテーマ難易度
Mission 1v1の問題分析と変更計画初級
Mission 2後方互換性の維持戦略中級
Mission 3v2 のAPI設計中級
Mission 4廃止スケジュールの策定中級
Mission 5マイグレーションガイド作成上級
Mission 6BFF設計の検討上級

Mission 1: v1の問題分析と変更計画(15分)

以下の v1 API の問題点を分析し、v2 での解決策を提案してください。

v1の問題点

// 問題1: ユーザー名が単一フィールド
GET /api/v1/users/123
{ "name": "田中太郎" }

// 問題2: エラーレスポンスが不統一
{ "error": "not found" }           // エンドポイントA
{ "message": "validation failed" }  // エンドポイントB
{ "err": { "code": 404 } }         // エンドポイントC

// 問題3: ページネーションがない
GET /api/v1/tasks  // 全件返す

// 問題4: 日付形式がバラバラ
{ "created": "2025/01/15", "updated": 1705305600 }

// 問題5: IDが数値
{ "id": 12345 }
解答
// 変更計画

// 問題1の解決: 姓名分離(Expand/Contract パターン)
// v2: firstName, lastName を追加、name は維持(互換性)
{
  "name": "田中太郎",        // 維持(後に廃止)
  "firstName": "太郎",       // 新規
  "lastName": "田中"         // 新規
}

// 問題2の解決: エラーレスポンスの統一(破壊的変更)
// v2: RFC 7807 準拠の統一形式
{
  "error": {
    "code": "NOT_FOUND",
    "message": "リソースが見つかりません",
    "traceId": "req_abc123"
  }
}

// 問題3の解決: ページネーション追加(後方互換)
// v2: page, perPage パラメータ追加(デフォルト値あり)
GET /api/v2/tasks?page=1&perPage=20
{
  "data": [...],
  "meta": { "totalCount": 42, "currentPage": 1, "perPage": 20, "totalPages": 3 }
}

// 問題4の解決: ISO 8601 統一(破壊的変更)
// v2: すべての日付をISO 8601に統一
{
  "createdAt": "2025-01-15T09:00:00Z",
  "updatedAt": "2025-01-15T14:30:00Z"
}

// 問題5の解決: 文字列ID(破壊的変更)
// v2: プレフィックス付き文字列ID
{ "id": "usr_123" }

// 変更の分類:
// 後方互換: ページネーション追加、姓名フィールド追加
// 破壊的変更: エラー形式統一、日付形式統一、ID型変更
// → 破壊的変更があるため、バージョンアップが必要

Mission 2: 後方互換性の維持戦略(15分)

v1からv2への移行中に、両バージョンを並行運用するための戦略を設計してください。

解答
// 並行運用の戦略

// 1. 共通のビジネスロジック層を維持
// v1, v2 のルーターが同じサービス層を利用
class UserService {
  async findById(id: string): Promise<InternalUser> {
    return this.repository.findById(id);
  }
}

// 2. バージョン別のレスポンス変換層
// v1 Controller
function toV1Response(user: InternalUser) {
  return {
    id: user.numericId,           // 数値ID
    name: `${user.lastName} ${user.firstName}`,  // 結合名
    created: formatDateSlash(user.createdAt),     // スラッシュ形式
  };
}

// v2 Controller
function toV2Response(user: InternalUser) {
  return {
    id: `usr_${user.numericId}`,   // 文字列ID
    firstName: user.firstName,
    lastName: user.lastName,
    name: `${user.lastName} ${user.firstName}`,  // 互換性のため残す
    createdAt: user.createdAt.toISOString(),      // ISO 8601
  };
}

// 3. ID変換レイヤー
// v1: 数値ID → v2: 文字列ID
function numericToStringId(numericId: number): string {
  return `usr_${numericId}`;
}

function stringToNumericId(stringId: string): number {
  return parseInt(stringId.replace('usr_', ''));
}

// 4. 共通ミドルウェア
// 両バージョンで共通の認証・レート制限を適用
app.use('/api/v1', authMiddleware, v1DeprecationMiddleware, v1Router);
app.use('/api/v2', authMiddleware, v2Router);

Mission 3: v2 のAPI設計(20分)

v1 の問題を解決した v2 のエンドポイントとレスポンス形式を設計してください。

要件

  • ユーザー取得、タスク一覧、タスク作成 の3つのエンドポイント
解答
// === ユーザー取得 ===
GET /api/v2/users/usr_123
Response: 200 OK
{
  "data": {
    "id": "usr_123",
    "firstName": "太郎",
    "lastName": "田中",
    "email": "tanaka@example.com",
    "avatarUrl": "https://example.com/avatars/123.jpg",
    "role": "member",
    "createdAt": "2025-01-15T09:00:00Z",
    "updatedAt": "2025-01-15T09:00:00Z"
  }
}

// === タスク一覧 ===
GET /api/v2/projects/proj_123/tasks?status=todo&sort=-priority&page=1&perPage=20
Response: 200 OK
{
  "data": [
    {
      "id": "tsk_001",
      "title": "ログイン画面の改善",
      "status": "todo",
      "priority": "high",
      "assignee": { "id": "usr_456", "name": "鈴木花子" },
      "dueDate": "2025-02-28T23:59:59Z",
      "createdAt": "2025-01-15T09:00:00Z"
    }
  ],
  "meta": {
    "totalCount": 42,
    "currentPage": 1,
    "perPage": 20,
    "totalPages": 3
  }
}

// === タスク作成 ===
POST /api/v2/projects/proj_123/tasks
Request:
{
  "title": "新機能の実装",
  "description": "GraphQL対応の実装",
  "priority": "high",
  "assigneeId": "usr_456",
  "dueDate": "2025-03-31T23:59:59Z"
}
Response: 201 Created
Location: /api/v2/tasks/tsk_002
{
  "data": {
    "id": "tsk_002",
    "title": "新機能の実装",
    "description": "GraphQL対応の実装",
    "status": "todo",
    "priority": "high",
    "assignee": { "id": "usr_456", "name": "鈴木花子" },
    "dueDate": "2025-03-31T23:59:59Z",
    "createdAt": "2025-01-20T10:00:00Z",
    "updatedAt": "2025-01-20T10:00:00Z"
  }
}

// === エラーレスポンス(全エンドポイント統一)===
// 422 Unprocessable Entity
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "入力内容に問題があります",
    "details": [
      { "field": "title", "message": "タイトルは必須です" }
    ],
    "traceId": "req_abc123"
  }
}

Mission 4: 廃止スケジュールの策定(15分)

v1の廃止スケジュールとタイムラインを作成してください。

解答
TaskFlow API v1 廃止スケジュール

■ 2025年7月1日: v2リリース & v1廃止告知
  - v2をリリース
  - v1のレスポンスにSunsetヘッダーを追加
  - マイグレーションガイドを公開
  - 開発者ブログで告知

■ 2025年7月〜9月: 移行支援期間
  - v1利用者への個別連絡
  - v1のアクセスログを監視
  - サポートチャンネルで移行相談に対応

■ 2025年10月1日: 警告強化
  - v1のレスポンスに _warnings フィールドを追加
  - v1のレート制限を 1000 req/hour → 500 req/hour に変更
  - 利用者にリマインダーメール送信

■ 2025年12月1日: 最終警告
  - v1のレート制限を 500 → 100 req/hour に変更
  - 残存利用者に最終警告メール
  - ステータスページで告知

■ 2025年12月31日: v1廃止
  - v1のすべてのエンドポイントが 410 Gone を返す
  - 移行ガイドのURLを含むエラーレスポンスを返す
  - v1のコードを保守モードに移行(セキュリティ修正のみ)

■ 2026年3月31日: v1コードの完全削除
  - v1関連のコードをリポジトリから削除

Mission 5: マイグレーションガイド作成(15分)

v1 から v2 への移行ガイドの骨格を作成してください。

解答
# TaskFlow API v1 → v2 マイグレーションガイド

## 概要
v2では以下の改善を行いました:
- レスポンス形式の統一
- ページネーションの標準化
- 日付形式のISO 8601統一
- IDの文字列化

## 主な変更点

### 1. IDの変更
| v1 | v2 | 対応方法 |
|----|-----|---------|
| `{ "id": 123 }` | `{ "id": "usr_123" }` | 文字列として扱う |

### 2. レスポンス形式の変更
| v1 | v2 |
|----|-----|
| `{ "name": "田中太郎" }` | `{ "firstName": "太郎", "lastName": "田中" }` |
| `{ "created": "2025/01/15" }` | `{ "createdAt": "2025-01-15T09:00:00Z" }` |

### 3. エラーレスポンスの変更
v2ではすべてのエラーが統一形式で返されます。

### 4. ページネーション
v2ではコレクションAPIに `page``perPage` パラメータが追加されました。

## コード修正例(TypeScript)

### Before (v1):
const res = await fetch('/api/v1/users/123');
const user = await res.json();
console.log(user.name);  // "田中太郎"

### After (v2):
const res = await fetch('/api/v2/users/usr_123');
const { data: user } = await res.json();
console.log(`${user.lastName} ${user.firstName}`);  // "田中 太郎"

## サポート
質問・問題: api-support@taskflow.example.com

Mission 6: BFF設計の検討(10分)

TaskFlow がWebアプリとモバイルアプリの2つのクライアントを持つ場合、BFFを導入すべきか検討してください。

解答
// 検討結果: BFF導入を推奨

// 理由:
// 1. Web版はプロジェクト管理画面が複雑(多数のデータ必要)
// 2. モバイル版はタスク操作が中心(最小限のデータ)
// 3. 帯域とパフォーマンスの最適化が必要

// 構成:
// Webアプリ → Web BFF → TaskFlow API
// モバイルアプリ → Mobile BFF → TaskFlow API

// Web BFF のダッシュボードエンドポイント
app.get('/web/dashboard', async (req, res) => {
  const [user, projects, recentTasks, stats, notifications] = await Promise.all([
    api.get(`/v2/users/me`),
    api.get(`/v2/projects?perPage=10`),
    api.get(`/v2/tasks?assignee=me&sort=-updatedAt&perPage=10`),
    api.get(`/v2/stats/dashboard`),
    api.get(`/v2/notifications?perPage=20`),
  ]);

  res.json({ user, projects, recentTasks, stats, notifications });
});

// Mobile BFF のダッシュボードエンドポイント
app.get('/mobile/dashboard', async (req, res) => {
  const [user, tasks, unreadCount] = await Promise.all([
    api.get(`/v2/users/me`),
    api.get(`/v2/tasks?assignee=me&sort=-updatedAt&perPage=3`),
    api.get(`/v2/notifications/unread-count`),
  ]);

  res.json({
    user: { name: user.data.firstName, avatarUrl: user.data.avatarUrl },
    tasks: tasks.data.map(t => ({ id: t.id, title: t.title, status: t.status })),
    unreadNotifications: unreadCount,
  });
});

// 代替案: GraphQLを導入してBFFの代わりにする
// → クライアントが必要なフィールドだけ取得可能
// → BFFの開発・保守コストを削減できる

達成度チェック

ミッションテーマ完了
Mission 1v1の問題分析と変更計画[ ]
Mission 2後方互換性の維持戦略[ ]
Mission 3v2のAPI設計[ ]
Mission 4廃止スケジュールの策定[ ]
Mission 5マイグレーションガイド作成[ ]
Mission 6BFF設計の検討[ ]

まとめ

ポイント内容
問題分析変更を後方互換と破壊的変更に分類する
並行運用共通のビジネスロジック層 + バージョン別変換層
廃止計画告知→移行→警告→廃止の段階的プロセス
マイグレーション変更点、コード例、サポート情報を含むガイド

チェックリスト

  • APIの問題点を分析し、変更計画を立てられた
  • v1/v2の並行運用戦略を設計できた
  • 廃止スケジュールを作成できた
  • マイグレーションガイドの構成を理解した

推定所要時間: 90分