EXERCISE 90分

演習:Copilotでアプリを構築しよう

ストーリー

「理論は十分だ。Copilotをフル活用して、ミニアプリを一つ作ってみよう」

中島先輩が課題を出した。

「TODOアプリのバックエンドAPIだ。Copilotの全機能を使って構築してくれ」

「全部Copilotで書くんですか?」

「いいや。設計は自分で考える。実装の手間をCopilotで省く。 この感覚を体で覚えてほしい」


課題: TODOアプリのバックエンドAPI構築

全体要件

技術スタック: TypeScript + Express.js
機能:
  - TODO の CRUD(作成、読取、更新、削除)
  - ステータス管理(pending, in_progress, done)
  - 優先度(high, medium, low)

評価ポイント:
  - Copilot の補完をどれだけ活用できたか
  - 型安全な実装になっているか
  - テストが含まれているか

Phase 1: 型定義を作成(15分)

やること

  1. types.ts ファイルを作成
  2. コメントで型の意図を記述
  3. Copilot に型定義を補完させる

最低限必要な型

- Todo: id, title, description, status, priority, createdAt, updatedAt
- CreateTodoInput: 作成時の入力
- UpdateTodoInput: 更新時の入力
- TodoStatus: 'pending' | 'in_progress' | 'done'
- Priority: 'high' | 'medium' | 'low'
<details> <summary>解答例(自分で実装してから確認しよう)</summary>
typescript
// types.ts

/** TODOのステータス */
type TodoStatus = 'pending' | 'in_progress' | 'done';

/** 優先度 */
type Priority = 'high' | 'medium' | 'low';

/** TODOエンティティ */
interface Todo {
  id: string;
  title: string;
  description: string;
  status: TodoStatus;
  priority: Priority;
  createdAt: Date;
  updatedAt: Date;
}

/** TODO作成の入力 */
interface CreateTodoInput {
  title: string;
  description?: string;
  priority?: Priority;
}

/** TODO更新の入力 */
interface UpdateTodoInput {
  title?: string;
  description?: string;
  status?: TodoStatus;
  priority?: Priority;
}

/** APIレスポンスの共通型 */
interface ApiResponse<T> {
  success: boolean;
  data?: T;
  error?: string;
}
</details>

Phase 2: ルーティングを実装(20分)

やること

  1. routes/todos.ts を作成
  2. 各エンドポイントのコメントを先に書く
  3. Copilotにルーティング実装を補完させる

必要なエンドポイント

GET    /api/todos         - 一覧取得(フィルタ: status, priority)
GET    /api/todos/:id     - 詳細取得
POST   /api/todos         - 作成
PUT    /api/todos/:id     - 更新
DELETE /api/todos/:id     - 削除
PATCH  /api/todos/:id/status - ステータス変更
<details> <summary>解答例(自分で実装してから確認しよう)</summary>
typescript
// routes/todos.ts
import { Router, Request, Response } from 'express';
import { Todo, CreateTodoInput, UpdateTodoInput, ApiResponse } from '../types';
import { randomUUID } from 'crypto';

const router = Router();
const todos: Todo[] = [];

// TODO一覧取得(フィルタ対応)
router.get('/', (req: Request, res: Response<ApiResponse<Todo[]>>) => {
  const { status, priority } = req.query;
  let filtered = [...todos];

  if (status) {
    filtered = filtered.filter(t => t.status === status);
  }
  if (priority) {
    filtered = filtered.filter(t => t.priority === priority);
  }

  res.json({ success: true, data: filtered });
});

// TODO詳細取得
router.get('/:id', (req: Request, res: Response<ApiResponse<Todo>>) => {
  const todo = todos.find(t => t.id === req.params.id);
  if (!todo) {
    return res.status(404).json({ success: false, error: 'TODOが見つかりません' });
  }
  res.json({ success: true, data: todo });
});

// TODO作成
router.post('/', (req: Request<{}, {}, CreateTodoInput>, res: Response<ApiResponse<Todo>>) => {
  const { title, description, priority } = req.body;

  if (!title || title.trim().length === 0) {
    return res.status(400).json({ success: false, error: 'タイトルは必須です' });
  }

  const now = new Date();
  const todo: Todo = {
    id: randomUUID(),
    title: title.trim(),
    description: description || '',
    status: 'pending',
    priority: priority || 'medium',
    createdAt: now,
    updatedAt: now,
  };

  todos.push(todo);
  res.status(201).json({ success: true, data: todo });
});

// TODO更新
router.put('/:id', (req: Request<{ id: string }, {}, UpdateTodoInput>, res: Response<ApiResponse<Todo>>) => {
  const index = todos.findIndex(t => t.id === req.params.id);
  if (index === -1) {
    return res.status(404).json({ success: false, error: 'TODOが見つかりません' });
  }

  todos[index] = {
    ...todos[index],
    ...req.body,
    updatedAt: new Date(),
  };

  res.json({ success: true, data: todos[index] });
});

// TODO削除
router.delete('/:id', (req: Request, res: Response<ApiResponse<null>>) => {
  const index = todos.findIndex(t => t.id === req.params.id);
  if (index === -1) {
    return res.status(404).json({ success: false, error: 'TODOが見つかりません' });
  }

  todos.splice(index, 1);
  res.json({ success: true, data: null });
});

export default router;
</details>

Phase 3: テストを生成(25分)

やること

  1. __tests__/todos.test.ts を作成
  2. Copilotの /tests コマンドを活用
  3. エッジケースのテストを自分で追加

テストすべき観点

- 一覧取得(フィルタあり/なし)
- 存在するIDでの詳細取得
- 存在しないIDでの404エラー
- 正常な作成
- タイトル未入力での作成(バリデーションエラー)
- ステータス更新
- 削除
<details> <summary>解答例(自分で実装してから確認しよう)</summary>
typescript
// __tests__/todos.test.ts
import request from 'supertest';
import app from '../app';

describe('TODO API', () => {
  let createdTodoId: string;

  describe('POST /api/todos', () => {
    it('should create a new todo', async () => {
      const res = await request(app)
        .post('/api/todos')
        .send({ title: 'テストTODO', priority: 'high' });

      expect(res.status).toBe(201);
      expect(res.body.success).toBe(true);
      expect(res.body.data.title).toBe('テストTODO');
      expect(res.body.data.status).toBe('pending');
      createdTodoId = res.body.data.id;
    });

    it('should return 400 for empty title', async () => {
      const res = await request(app)
        .post('/api/todos')
        .send({ title: '' });

      expect(res.status).toBe(400);
      expect(res.body.success).toBe(false);
    });
  });

  describe('GET /api/todos', () => {
    it('should return all todos', async () => {
      const res = await request(app).get('/api/todos');
      expect(res.status).toBe(200);
      expect(res.body.data).toBeInstanceOf(Array);
    });
  });

  describe('GET /api/todos/:id', () => {
    it('should return 404 for non-existent id', async () => {
      const res = await request(app).get('/api/todos/non-existent-id');
      expect(res.status).toBe(404);
    });
  });

  describe('DELETE /api/todos/:id', () => {
    it('should delete a todo', async () => {
      const res = await request(app).delete(`/api/todos/${createdTodoId}`);
      expect(res.status).toBe(200);
      expect(res.body.success).toBe(true);
    });
  });
});
</details>

Phase 4: Copilot Chatでリファクタリング(30分)

やること

  1. Phase 2で作成したコードをCopilot Chatでレビュー
  2. 指摘に基づいてリファクタリング
  3. バリデーションの追加(zodの活用など)
<details> <summary>解答例(自分で実装してから確認しよう)</summary>

Copilot Chat への質問例:

@workspace 現在のtodos.tsのコードをレビューして、
以下の観点で改善してください:
1. バリデーションをzodで実装
2. エラーハンドリングの統一
3. ビジネスロジックをルーターから分離
</details>

達成度チェック

Phaseテーマ完了
Phase 1型定義の作成[ ]
Phase 2ルーティング実装[ ]
Phase 3テスト生成[ ]
Phase 4リファクタリング[ ]

まとめ

ポイント内容
設計は人間型定義やエンドポイント設計は自分で考える
実装はCopilotコメント駆動とコンテキスト設定で効率化
テストは協働骨格はAI、エッジケースは人間が追加
レビューはChatCopilot Chatで改善ポイントを洗い出す

チェックリスト

  • 型定義をコメント駆動で生成できた
  • CRUDエンドポイントをCopilotの補完で実装できた
  • /tests でテストコードを生成し、エッジケースを追加した
  • Copilot Chatでレビュー・リファクタリングを実行した

次のステップへ

お疲れさまでした。Copilotでのアプリ構築演習が完了しました。 次はStep 4のチェックポイントです。


推定読了時間: 90分