ストーリー
高橋アーキテクトが v1 の問題点リストを渡した。
ミッション概要
| ミッション | テーマ | 難易度 |
|---|---|---|
| Mission 1 | v1の問題分析と変更計画 | 初級 |
| Mission 2 | 後方互換性の維持戦略 | 中級 |
| Mission 3 | v2 のAPI設計 | 中級 |
| Mission 4 | 廃止スケジュールの策定 | 中級 |
| Mission 5 | マイグレーションガイド作成 | 上級 |
| Mission 6 | BFF設計の検討 | 上級 |
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 1 | v1の問題分析と変更計画 | [ ] |
| Mission 2 | 後方互換性の維持戦略 | [ ] |
| Mission 3 | v2のAPI設計 | [ ] |
| Mission 4 | 廃止スケジュールの策定 | [ ] |
| Mission 5 | マイグレーションガイド作成 | [ ] |
| Mission 6 | BFF設計の検討 | [ ] |
まとめ
| ポイント | 内容 |
|---|---|
| 問題分析 | 変更を後方互換と破壊的変更に分類する |
| 並行運用 | 共通のビジネスロジック層 + バージョン別変換層 |
| 廃止計画 | 告知→移行→警告→廃止の段階的プロセス |
| マイグレーション | 変更点、コード例、サポート情報を含むガイド |
チェックリスト
- APIの問題点を分析し、変更計画を立てられた
- v1/v2の並行運用戦略を設計できた
- 廃止スケジュールを作成できた
- マイグレーションガイドの構成を理解した
推定所要時間: 90分