🚀 2025年最新！NestJS + Prisma で作る爆速TodoアプリAPI開発【完全ガイド】


## 📌 はじめに

こんにちは！今回は **NestJS** と **Prisma** を使って、モダンなTodoアプリのバックエンドAPIを一から構築していきます。

「APIってどうやって作るの？」「NestJSって何？」という初心者の方でも、**この記事を読み終わる頃には本格的なRESTful APIが作れるようになります！**

:::note info
この記事は初心者向けに書かれています。コードを完全に理解する必要はありません。まずは「動くものを作る」ことを目標にしましょう！
:::

## 🎯 今回作るもの

**機能一覧**
- ✅ Todo一覧取得API
- ✅ Todo作成API  
- ✅ Todo更新API（完了状態の切り替えなど）
- ✅ Todo削除API
- ✅ PostgreSQLでのデータ永続化
- ✅ 自動API仕様書生成（Swagger）

**使用技術スタック**
- **バックエンド**: NestJS 11.x
- **ORM**: Prisma 6.x
- **データベース**: PostgreSQL 15
- **コンテナ**: Docker Compose
- **言語**: TypeScript

## 📦 環境バージョン

```json
{
  "node": "20.x以上",
  "nestjs": "11.0.1",
  "prisma": "6.8.2",
  "@prisma/client": "6.8.2",
  "postgresql": "15",
  "typescript": "5.7.3"
}
```

## 🛠️ 開発準備

### 必要ツールのインストール

```bash
# Node.js (LTS版推奨)
# Docker & Docker Compose
# VS Code (推奨エディタ)
# Postman または Thunder Client (API テスト用)
```

### プロジェクト構成

```
nest-react-todo-app/
├── backend/              # NestJS アプリケーション
├── docker-compose.yml    # PostgreSQL設定
└── README.md
```

## 🐘 Phase 1: PostgreSQL環境構築

最初にデータベース環境を整えましょう。Docker Composeを使って簡単にPostgreSQLを起動できます。

### docker-compose.yml作成

ルートディレクトリに以下のファイルを作成してください：

```yaml
version: '3.8'

services:
  postgres:
    image: postgres:15
    container_name: todo-postgres
    environment:
      # データベースの基本設定
      POSTGRES_DB: todoapp          # データベース名
      POSTGRES_USER: todouser       # ユーザー名
      POSTGRES_PASSWORD: todopass   # パスワード
    ports:
      - "5434:5432"  # ホスト:コンテナのポート番号
    volumes:
      # データを永続化するためのボリューム
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  # PostgreSQLのデータを保存するボリューム
  postgres_data:
```

### PostgreSQL起動

```bash
# データベース起動
docker-compose up -d postgres

# 起動確認
docker-compose ps
```

:::note info
ポート番号を5434にしているのは、ローカルにPostgreSQLがインストールされている場合の競合を避けるためです。
:::

## 🏗️ Phase 2: NestJSプロジェクト作成

### NestJS CLI インストール & プロジェクト作成

```bash
# NestJS CLI をグローバルインストール
npm i -g @nestjs/cli

# プロジェクト作成
nest new backend
cd backend
```

### 必要な依存関係インストール

```bash
# Prisma関連
npm install prisma @prisma/client

# 開発用依存関係
npm install -D prisma
```

## 🗃️ Phase 3: Prisma設定

### Prisma初期化

```bash
# Prisma初期化
npx prisma init
```

### 環境変数設定

`.env`ファイルを以下の内容に更新してください：

```env
# Database
DATABASE_URL="postgresql://todouser:todopass@localhost:5434/todoapp"
```

### データベーススキーマ定義

`prisma/schema.prisma`を以下の内容に更新してください：

```prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

// Todoテーブルの定義
model Todo {
  id          Int      @id @default(autoincrement()) // 主キー、自動採番
  title       String                                  // タイトル（必須）
  description String?                                 // 説明（任意）
  completed   Boolean  @default(false)                // 完了状態（デフォルト：未完了）
  createdAt   DateTime @default(now())                // 作成日時
  updatedAt   DateTime @updatedAt                     // 更新日時

  @@map("todos") // テーブル名を「todos」に設定
}
```

### マイグレーション実行

```bash
# マイグレーション実行（初回）
npx prisma migrate dev --name init

# Prismaクライアント生成
npx prisma generate
```

:::note info
マイグレーションとは、データベースのスキーマ変更を管理する仕組みです。Prismaが自動でSQLを生成してくれます！
:::

## 🔧 Phase 4: PrismaService作成

NestJSでPrismaを使うためのサービスを作成します。

### Prismaサービス生成

```bash
# Prismaサービス生成
nest g service prisma --no-spec
```

### PrismaService実装

`src/prisma/prisma.service.ts`を以下の内容に更新してください：

```typescript
/**
 * prisma.service.ts
 * 
 * Prismaクライアントの管理を行うサービス
 * データベースへの接続と切断を自動で管理します
 */

import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  /**
   * モジュール初期化時にデータベースに接続
   */
  async onModuleInit() {
    await this.$connect();
    console.log('✅ データベースに接続しました');
  }

  /**
   * アプリケーション終了時にデータベース接続を切断
   */
  async onModuleDestroy() {
    await this.$disconnect();
    console.log('✅ データベース接続を切断しました');
  }
}
```

## 📝 Phase 5: Todo機能実装

### Todoモジュール生成

```bash
# Todoモジュール・コントローラー・サービスを一括生成
nest g resource todo --no-spec
```

:::note info
`nest g resource`コマンドは、モジュール、コントローラー、サービス、DTOファイルを一度に生成してくれる便利なコマンドです！
:::

### 型定義（DTO）作成

まず、API通信で使用する型を定義しましょう。

#### CreateTodoDto

`src/todo/dto/create-todo.dto.ts`：

```typescript
/**
 * create-todo.dto.ts
 * 
 * 新規Todo作成時のリクエストデータの型定義
 */

import { IsString, IsOptional, IsNotEmpty, MaxLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class CreateTodoDto {
  @ApiProperty({
    description: 'Todoのタイトル',
    example: '買い物に行く',
    maxLength: 100
  })
  @IsString({ message: 'タイトルは文字列である必要があります' })
  @IsNotEmpty({ message: 'タイトルは必須です' })
  @MaxLength(100, { message: 'タイトルは100文字以内で入力してください' })
  title: string;

  @ApiProperty({
    description: 'Todoの詳細説明',
    example: '牛乳とパンを買う',
    required: false
  })
  @IsOptional()
  @IsString({ message: '説明は文字列である必要があります' })
  @MaxLength(500, { message: '説明は500文字以内で入力してください' })
  description?: string;
}
```

#### UpdateTodoDto

`src/todo/dto/update-todo.dto.ts`：

```typescript
/**
 * update-todo.dto.ts
 * 
 * Todo更新時のリクエストデータの型定義
 * 全てのフィールドがオプション（部分更新を可能にするため）
 */

import { PartialType } from '@nestjs/mapped-types';
import { CreateTodoDto } from './create-todo.dto';
import { IsBoolean, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class UpdateTodoDto extends PartialType(CreateTodoDto) {
  @ApiProperty({
    description: 'Todoの完了状態',
    example: true,
    required: false
  })
  @IsOptional()
  @IsBoolean({ message: '完了状態はtrue/falseで指定してください' })
  completed?: boolean;
}
```

### TodoService実装

`src/todo/todo.service.ts`を以下の内容に更新してください：

```typescript
/**
 * todo.service.ts
 *
 * Todo機能のサービス
 * ビジネスロジックの実装とデータベース操作を担当します。
 * Prismaを使用してデータベースとのやり取りを行います。
 */

import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateTodoDto } from './dto/create-todo.dto';
import { UpdateTodoDto } from './dto/update-todo.dto';
import { Todo } from '@prisma/client';

@Injectable()
export class TodoService {
  // PrismaServiceを依存注入
  constructor(private prisma: PrismaService) {}

  /**
   * 新しいTodoを作成
   *
   * @param {CreateTodoDto} createTodoDto - 作成するTodoのデータ
   * @returns {Promise<Todo>} 作成されたTodo
   */
  async create(createTodoDto: CreateTodoDto): Promise<Todo> {
    try {
      const todo = await this.prisma.todo.create({
        data: {
          title: createTodoDto.title,
          description: createTodoDto.description,
        },
      });

      console.log(`✅ Todo作成完了: ID=${todo.id}, Title="${todo.title}"`);
      return todo;
    } catch (error) {
      console.error('❌ Todo作成エラー:', error);
      throw new Error('Todoの作成に失敗しました');
    }
  }

  /**
   * 全てのTodoを取得
   * 作成日時の降順でソート
   *
   * @returns {Promise<Todo[]>} Todoの配列
   */
  async findAll(): Promise<Todo[]> {
    try {
      const todos = await this.prisma.todo.findMany({
        orderBy: {
          createdAt: 'desc', // 新しい順で取得
        },
      });

      console.log(`✅ Todo一覧取得完了: ${todos.length}件`);
      return todos;
    } catch (error) {
      console.error('❌ Todo一覧取得エラー:', error);
      throw new Error('Todoの取得に失敗しました');
    }
  }

  /**
   * 特定のIDのTodoを取得
   *
   * @param {number} id - 取得するTodoのID
   * @returns {Promise<Todo>} 取得したTodo
   * @throws {NotFoundException} Todoが見つからない場合
   */
  async findOne(id: number): Promise<Todo> {
    try {
      const todo = await this.prisma.todo.findUnique({
        where: { id },
      });

      if (!todo) {
        throw new NotFoundException(`ID=${id}のTodoが見つかりません`);
      }

      console.log(`✅ Todo取得完了: ID=${todo.id}, Title="${todo.title}"`);
      return todo;
    } catch (error) {
      if (error instanceof NotFoundException) {
        throw error;
      }
      console.error('❌ Todo取得エラー:', error);
      throw new Error('Todoの取得に失敗しました');
    }
  }

  /**
   * Todoを更新
   *
   * @param {number} id - 更新するTodoのID
   * @param {UpdateTodoDto} updateTodoDto - 更新するデータ
   * @returns {Promise<Todo>} 更新されたTodo
   * @throws {NotFoundException} Todoが見つからない場合
   */
  async update(id: number, updateTodoDto: UpdateTodoDto): Promise<Todo> {
    try {
      // まず対象のTodoが存在するかチェック
      await this.findOne(id);

      const todo = await this.prisma.todo.update({
        where: { id },
        data: updateTodoDto,
      });

      console.log(`✅ Todo更新完了: ID=${todo.id}, Title="${todo.title}"`);
      return todo;
    } catch (error) {
      if (error instanceof NotFoundException) {
        throw error;
      }
      console.error('❌ Todo更新エラー:', error);
      throw new Error('Todoの更新に失敗しました');
    }
  }

  /**
   * Todoを削除
   *
   * @param {number} id - 削除するTodoのID
   * @returns {Promise<Todo>} 削除されたTodo
   * @throws {NotFoundException} Todoが見つからない場合
   */
  async remove(id: number): Promise<Todo> {
    try {
      // まず対象のTodoが存在するかチェック
      await this.findOne(id);

      const todo = await this.prisma.todo.delete({
        where: { id },
      });

      console.log(`✅ Todo削除完了: ID=${todo.id}, Title="${todo.title}"`);
      return todo;
    } catch (error) {
      if (error instanceof NotFoundException) {
        throw error;
      }
      console.error('❌ Todo削除エラー:', error);
      throw new Error('Todoの削除に失敗しました');
    }
  }
}
```

### TodoController実装

`src/todo/todo.controller.ts`を以下の内容に更新してください：

```typescript
/**
 * todo.controller.ts
 *
 * Todo機能のコントローラー
 * HTTPリクエストを受け取り、適切なサービスメソッドを呼び出します
 * RESTful APIの設計に従ったエンドポイントを提供します
 */

import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
  ParseIntPipe,
  HttpCode,
  HttpStatus,
} from '@nestjs/common';
import { TodoService } from './todo.service';
import { CreateTodoDto } from './dto/create-todo.dto';
import { UpdateTodoDto } from './dto/update-todo.dto';
import {
  ApiTags,
  ApiOperation,
  ApiResponse,
  ApiParam,
} from '@nestjs/swagger';

@ApiTags('todos')
@Controller('todos')
export class TodoController {
  constructor(private readonly todoService: TodoService) {}

  /**
   * 新しいTodoを作成
   * POST /todos
   */
  @Post()
  @HttpCode(HttpStatus.CREATED)
  @ApiOperation({ summary: '新しいTodoを作成' })
  @ApiResponse({
    status: 201,
    description: 'Todoが正常に作成されました',
  })
  @ApiResponse({
    status: 400,
    description: 'リクエストデータが不正です',
  })
  async create(@Body() createTodoDto: CreateTodoDto) {
    return this.todoService.create(createTodoDto);
  }

  /**
   * 全てのTodoを取得
   * GET /todos
   */
  @Get()
  @ApiOperation({ summary: '全てのTodoを取得' })
  @ApiResponse({
    status: 200,
    description: 'Todo一覧を正常に取得しました',
  })
  async findAll() {
    return this.todoService.findAll();
  }

  /**
   * 特定のTodoを取得
   * GET /todos/:id
   */
  @Get(':id')
  @ApiOperation({ summary: '特定のTodoを取得' })
  @ApiParam({
    name: 'id',
    description: 'TodoのID',
    type: 'number',
  })
  @ApiResponse({
    status: 200,
    description: 'Todoを正常に取得しました',
  })
  @ApiResponse({
    status: 404,
    description: '指定されたTodoが見つかりません',
  })
  async findOne(@Param('id', ParseIntPipe) id: number) {
    return this.todoService.findOne(id);
  }

  /**
   * Todoを更新
   * PATCH /todos/:id
   */
  @Patch(':id')
  @ApiOperation({ summary: 'Todoを更新' })
  @ApiParam({
    name: 'id',
    description: 'TodoのID',
    type: 'number',
  })
  @ApiResponse({
    status: 200,
    description: 'Todoが正常に更新されました',
  })
  @ApiResponse({
    status: 404,
    description: '指定されたTodoが見つかりません',
  })
  async update(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateTodoDto: UpdateTodoDto,
  ) {
    return this.todoService.update(id, updateTodoDto);
  }

  /**
   * Todoを削除
   * DELETE /todos/:id
   */
  @Delete(':id')
  @ApiOperation({ summary: 'Todoを削除' })
  @ApiParam({
    name: 'id',
    description: 'TodoのID',
    type: 'number',
  })
  @ApiResponse({
    status: 200,
    description: 'Todoが正常に削除されました',
  })
  @ApiResponse({
    status: 404,
    description: '指定されたTodoが見つかりません',
  })
  async remove(@Param('id', ParseIntPipe) id: number) {
    return this.todoService.remove(id);
  }
}
```

### AppModule更新

`src/app.module.ts`を以下の内容に更新してください：

```typescript
/**
 * app.module.ts
 *
 * アプリケーションのルートモジュール
 * アプリケーション全体の依存関係を管理します。
 * 各機能モジュールをインポートし、アプリケーション全体を構成します。
 */

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodoModule } from './todo/todo.module';
import { PrismaService } from './prisma/prisma.service';

/**
 * AppModule
 *
 * アプリケーションのルートモジュール
 * 以下のコンポーネントを統合します：
 * - Controllers: リクエストのハンドリング
 * - Services: ビジネスロジックの実装
 * - Providers: 依存性の注入
 */
@Module({
  imports: [TodoModule],
  controllers: [AppController],
  providers: [AppService, PrismaService],
})
export class AppModule {}
```

### TodoModule更新

`src/todo/todo.module.ts`を以下の内容に更新してください：

```typescript
/**
 * todo.module.ts
 * 
 * Todo機能のモジュール
 * Todo関連のコントローラー、サービス、依存関係を管理します
 */

import { Module } from '@nestjs/common';
import { TodoService } from './todo.service';
import { TodoController } from './todo.controller';
import { PrismaService } from '../prisma/prisma.service';

@Module({
  controllers: [TodoController],
  providers: [TodoService, PrismaService],
})
export class TodoModule {}
```

## 🌐 Phase 6: CORS設定とSwagger設定

### main.ts設定

`src/main.ts`を以下の内容に更新してください：

```typescript
/**
 * main.ts
 * 
 * NestJSアプリケーションのエントリーポイント
 * サーバーの起動とグローバル設定を行います
 */

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // バリデーションパイプの設定
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true, // DTOで定義されていないプロパティを除去
      forbidNonWhitelisted: true, // 不正なプロパティがある場合エラー
      transform: true, // リクエストデータを自動的にDTOインスタンスに変換
    }),
  );

  // CORS を有効化
  app.enableCors({
    origin: ['http://localhost:5173', 'http://localhost:3000'], // Reactアプリのオリジンを許可
    methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], // 許可するHTTPメソッド
    allowedHeaders: ['Content-Type', 'Authorization'], // 許可するヘッダー
    credentials: true, // クッキーなどの認証情報を含むリクエストを許可
  });

  // Swagger設定
  const config = new DocumentBuilder()
    .setTitle('Todo API')
    .setDescription('NestJS + Prisma で作るTodoアプリのAPI仕様書')
    .setVersion('1.0')
    .addTag('todos', 'Todo関連のAPI')
    .build();

  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document);

  const port = process.env.PORT ?? 3001;
  await app.listen(port);

  console.log(`🚀 サーバーが起動しました: http://localhost:${port}`);
  console.log(`📚 API仕様書: http://localhost:${port}/api`);
}

bootstrap().catch((error) => {
  console.error('❌ アプリケーションの起動に失敗しました:', error);
  process.exit(1);
});
```

## 🧪 Phase 7: 動作確認

### サーバー起動

```bash
# 開発モードでサーバー起動
npm run start:dev
```

コンソールに以下のようなメッセージが表示されれば成功です：

```
✅ データベースに接続しました
🚀 サーバーが起動しました: http://localhost:3001
📚 API仕様書: http://localhost:3001/api
```

### Swagger UIでAPI確認

ブラウザで `http://localhost:3001/api` にアクセスしてください。

以下のようなSwagger UIが表示され、各APIエンドポイントを確認・テストできます：

- `POST /todos` - Todo作成
- `GET /todos` - Todo一覧取得
- `GET /todos/{id}` - 特定Todo取得
- `PATCH /todos/{id}` - Todo更新
- `DELETE /todos/{id}` - Todo削除

### APIテスト手順

#### 1. Todo作成テスト

Swagger UIで `POST /todos` をクリック：

```json
{
  "title": "買い物に行く",
  "description": "牛乳とパンを買う"
}
```

#### 2. Todo一覧取得テスト

`GET /todos` で作成したTodoが表示されることを確認

#### 3. Todo更新テスト

`PATCH /todos/{id}` で完了状態を更新：

```json
{
  "completed": true
}
```

#### 4. Todo削除テスト

`DELETE /todos/{id}` で作成したTodoを削除

### Postman/Thunder Clientでのテスト

#### Todo作成

```
POST http://localhost:3001/todos
Content-Type: application/json

{
  "title": "プログラミング学習",
  "description": "NestJSとPrismaを学ぶ"
}
```

#### Todo一覧取得

```
GET http://localhost:3001/todos
```

## 🐛 トラブルシューティング

### よくあるエラーと解決方法

#### 1. データベース接続エラー

```
Error: Can't reach database server
```

**解決方法:**
```bash
# PostgreSQLコンテナが起動しているか確認
docker-compose ps

# 停止している場合は起動
docker-compose up -d postgres

# .envファイルのDATABASE_URLを確認
```

#### 2. ポート競合エラー

```
Error: listen EADDRINUSE: address already in use :::3001
```

**解決方法:**
```bash
# 使用中のプロセスを確認
lsof -i :3001

# プロセスを終了
kill -9 <PID>

# または別のポートを使用
PORT=3002 npm run start:dev
```

#### 3. Prismaマイグレーションエラー

```
Error: Migration failed
```

**解決方法:**
```bash
# マイグレーションファイルをリセット
npx prisma migrate reset

# 再度マイグレーション実行
npx prisma migrate dev --name init

# Prismaクライアント再生成
npx prisma generate
```

#### 4. バリデーションエラー

```
400 Bad Request: validation failed
```

**解決方法:**
- リクエストボディのフィールド名とデータ型を確認
- 必須フィールドが入力されているかチェック
- 文字数制限を超えていないか確認

#### 5. モジュールが見つからないエラー

```
Module not found: Cannot resolve '@prisma/client'
```

**解決方法:**
```bash
# node_modulesを削除して再インストール
rm -rf node_modules package-lock.json
npm install

# Prismaクライアント再生成
npx prisma generate
```

## 📊 データベース確認方法

### Prisma Studio使用

```bash
# Prisma Studioを起動（GUIでデータベース確認）
npx prisma studio
```

ブラウザで `http://localhost:5555` にアクセスしてデータを視覚的に確認できます。

### SQL直接実行

```bash
# PostgreSQLコンテナに接続
docker exec -it todo-postgres psql -U todouser -d todoapp

# Todoテーブル確認
\dt
SELECT * FROM todos;
```

## 🎉 完成！

お疲れ様でした！これで本格的なTodoアプリのバックエンドAPIが完成です。

### 実装した機能の振り返り

✅ **CRUD操作完備**: Create, Read, Update, Delete の全機能  
✅ **型安全性**: TypeScriptとPrismaによる完全な型保護  
✅ **バリデーション**: class-validatorによる入力検証  
✅ **API仕様書**: Swaggerによる自動ドキュメント生成  
✅ **エラーハンドリング**: 適切なHTTPステータスコードとエラーメッセージ  
✅ **データベース永続化**: PostgreSQLによるデータ保存  

### 次のステップ

1. **フロントエンド開発**: React + TypeScriptでUIを作成
2. **認証機能**: JWT認証の実装
3. **テスト**: Jest/Supertest による自動テスト
4. **デプロイ**: Heroku/Vercel等への本番環境デプロイ

## 🔧 追加機能の実装例

### Todo検索機能

検索機能を追加したい場合の実装例：

#### SearchTodoDto作成

`src/todo/dto/search-todo.dto.ts`：

```typescript
import { IsString, IsNotEmpty, MinLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class SearchTodoDto {
  @ApiProperty({
    description: '検索キーワード',
    example: '買い物',
    minLength: 1
  })
  @IsString({ message: '検索キーワードは文字列である必要があります' })
  @IsNotEmpty({ message: '検索キーワードは必須です' })
  @MinLength(1, { message: '検索キーワードは1文字以上入力してください' })
  keyword: string;
}
```

#### TodoServiceに検索メソッド追加

```typescript
/**
 * Todoを検索
 * タイトルまたは説明文に検索キーワードが含まれるTodoを取得
 */
async searchTodos(keyword: string): Promise<Todo[]> {
  try {
    const todos = await this.prisma.todo.findMany({
      where: {
        OR: [
          {
            title: {
              contains: keyword,
              mode: 'insensitive', // 大文字小文字を区別しない
            },
          },
          {
            description: {
              contains: keyword,
              mode: 'insensitive',
            },
          },
        ],
      },
      orderBy: {
        createdAt: 'desc',
      },
    });

    console.log(`✅ Todo検索完了: キーワード="${keyword}", ${todos.length}件`);
    return todos;
  } catch (error) {
    console.error('❌ Todo検索エラー:', error);
    throw new Error('Todoの検索に失敗しました');
  }
}
```

#### TodoControllerに検索エンドポイント追加

```typescript
/**
 * Todoを検索
 * POST /todos/search
 */
@Post('search')
@ApiOperation({ summary: 'Todoを検索' })
@ApiResponse({
  status: 200,
  description: '検索結果を正常に取得しました',
})
async searchTodos(@Body() searchTodoDto: SearchTodoDto) {
  return this.todoService.searchTodos(searchTodoDto.keyword);
}
```

### ページネーション機能

大量のTodoを効率的に取得するためのページネーション：

#### PaginationDto作成

`src/todo/dto/pagination.dto.ts`：

```typescript
import { IsOptional, IsInt, Min, Max } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';

export class PaginationDto {
  @ApiProperty({
    description: 'ページ番号',
    example: 1,
    minimum: 1,
    required: false,
    default: 1
  })
  @IsOptional()
  @Type(() => Number)
  @IsInt({ message: 'ページ番号は整数である必要があります' })
  @Min(1, { message: 'ページ番号は1以上である必要があります' })
  page?: number = 1;

  @ApiProperty({
    description: '1ページあたりの件数',
    example: 10,
    minimum: 1,
    maximum: 100,
    required: false,
    default: 10
  })
  @IsOptional()
  @Type(() => Number)
  @IsInt({ message: '件数は整数である必要があります' })
  @Min(1, { message: '件数は1以上である必要があります' })
  @Max(100, { message: '件数は100以下である必要があります' })
  limit?: number = 10;
}
```

#### ページネーション対応メソッド

```typescript
/**
 * ページネーション付きでTodoを取得
 */
async findAllWithPagination(page: number = 1, limit: number = 10) {
  const skip = (page - 1) * limit;

  try {
    const [todos, total] = await Promise.all([
      this.prisma.todo.findMany({
        skip,
        take: limit,
        orderBy: {
          createdAt: 'desc',
        },
      }),
      this.prisma.todo.count(),
    ]);

    const totalPages = Math.ceil(total / limit);
    const hasNext = page < totalPages;
    const hasPrev = page > 1;

    return {
      todos,
      pagination: {
        currentPage: page,
        totalPages,
        totalItems: total,
        itemsPerPage: limit,
        hasNext,
        hasPrev,
      },
    };
  } catch (error) {
    console.error('❌ ページネーション取得エラー:', error);
    throw new Error('Todoの取得に失敗しました');
  }
}
```

## 📈 パフォーマンス最適化

### データベースインデックス

`prisma/schema.prisma`にインデックスを追加：

```prisma
model Todo {
  id          Int      @id @default(autoincrement())
  title       String
  description String?
  completed   Boolean  @default(false)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  // インデックス追加
  @@index([completed])
  @@index([createdAt])
  @@index([title])
  @@map("todos")
}
```

### 接続プール最適化

`prisma/schema.prisma`にデータベース接続設定を追加：

```prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  // 接続プール設定
  directUrl = env("DIRECT_URL")
}
```

`.env`に設定追加：

```env
DATABASE_URL="postgresql://todouser:todopass@localhost:5434/todoapp?connection_limit=5&pool_timeout=10"
DIRECT_URL="postgresql://todouser:todopass@localhost:5434/todoapp"
```

## 🛡️ セキュリティ強化

### レート制限

```bash
npm install @nestjs/throttler
```

`src/app.module.ts`に追加：

```typescript
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import { APP_GUARD } from '@nestjs/core';

@Module({
  imports: [
    ThrottlerModule.forRoot([
      {
        ttl: 60000, // 1分間
        limit: 10,  // 10リクエストまで
      },
    ]),
    TodoModule,
  ],
  providers: [
    {
      provide: APP_GUARD,
      useClass: ThrottlerGuard,
    },
    AppService,
    PrismaService,
  ],
})
export class AppModule {}
```

### 入力サニタイゼーション

```bash
npm install class-sanitizer
```

DTOにサニタイゼーション追加：

```typescript
import { Trim, Escape } from 'class-sanitizer';

export class CreateTodoDto {
  @Trim()
  @Escape()
  @IsString()
  @IsNotEmpty()
  title: string;

  @Trim()
  @Escape()
  @IsOptional()
  @IsString()
  description?: string;
}
```

## 🧪 テスト実装

### ユニットテスト例

`src/todo/todo.service.spec.ts`：

```typescript
import { Test, TestingModule } from '@nestjs/testing';
import { TodoService } from './todo.service';
import { PrismaService } from '../prisma/prisma.service';

describe('TodoService', () => {
  let service: TodoService;
  let prisma: PrismaService;

  const mockPrismaService = {
    todo: {
      create: jest.fn(),
      findMany: jest.fn(),
      findUnique: jest.fn(),
      update: jest.fn(),
      delete: jest.fn(),
    },
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        TodoService,
        {
          provide: PrismaService,
          useValue: mockPrismaService,
        },
      ],
    }).compile();

    service = module.get<TodoService>(TodoService);
    prisma = module.get<PrismaService>(PrismaService);
  });

  describe('create', () => {
    it('新しいTodoを作成できること', async () => {
      const createTodoDto = {
        title: 'テストタスク',
        description: 'テスト用の説明',
      };

      const mockTodo = {
        id: 1,
        ...createTodoDto,
        completed: false,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      mockPrismaService.todo.create.mockResolvedValue(mockTodo);

      const result = await service.create(createTodoDto);

      expect(result).toEqual(mockTodo);
      expect(mockPrismaService.todo.create).toHaveBeenCalledWith({
        data: createTodoDto,
      });
    });
  });

  describe('findAll', () => {
    it('全てのTodoを取得できること', async () => {
      const mockTodos = [
        {
          id: 1,
          title: 'タスク1',
          description: '説明1',
          completed: false,
          createdAt: new Date(),
          updatedAt: new Date(),
        },
      ];

      mockPrismaService.todo.findMany.mockResolvedValue(mockTodos);

      const result = await service.findAll();

      expect(result).toEqual(mockTodos);
      expect(mockPrismaService.todo.findMany).toHaveBeenCalledWith({
        orderBy: { createdAt: 'desc' },
      });
    });
  });
});
```

### E2Eテスト例

`test/todo.e2e-spec.ts`：

```typescript
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';

describe('TodoController (e2e)', () => {
  let app: INestApplication;

  beforeEach(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  afterAll(async () => {
    await app.close();
  });

  describe('/todos (POST)', () => {
    it('新しいTodoを作成', () => {
      return request(app.getHttpServer())
        .post('/todos')
        .send({
          title: 'E2Eテストタスク',
          description: 'E2Eテスト用の説明',
        })
        .expect(201)
        .expect((res) => {
          expect(res.body.title).toBe('E2Eテストタスク');
          expect(res.body.completed).toBe(false);
        });
    });
  });

  describe('/todos (GET)', () => {
    it('Todo一覧を取得', () => {
      return request(app.getHttpServer())
        .get('/todos')
        .expect(200)
        .expect((res) => {
          expect(Array.isArray(res.body)).toBe(true);
        });
    });
  });
});
```

## 🚀 デプロイ準備

### Docker設定

`Dockerfile`：

```dockerfile
# Node.js 20 LTS を使用
FROM node:20-alpine

# 作業ディレクトリを設定
WORKDIR /app

# package.jsonとpackage-lock.jsonをコピー
COPY package*.json ./

# 依存関係をインストール
RUN npm ci --only=production

# ソースコードをコピー
COPY . .

# Prismaクライアントを生成
RUN npx prisma generate

# アプリケーションをビルド
RUN npm run build

# ポートを公開
EXPOSE 3001

# アプリケーションを起動
CMD ["npm", "run", "start:prod"]
```

### 本番用docker-compose.yml

`docker-compose.prod.yml`：

```yaml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3001:3001"
    environment:
      - DATABASE_URL=postgresql://todouser:todopass@postgres:5432/todoapp
      - NODE_ENV=production
    depends_on:
      - postgres

  postgres:
    image: postgres:15
    environment:
      POSTGRES_DB: todoapp
      POSTGRES_USER: todouser
      POSTGRES_PASSWORD: todopass
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:
```

### 環境変数設定

`.env.production`：

```env
NODE_ENV=production
DATABASE_URL="postgresql://todouser:todopass@postgres:5432/todoapp"
PORT=3001
```

## 🎯 まとめ

この記事では、**NestJS + Prisma + PostgreSQL** を使って本格的なTodoアプリのバックエンドAPIを構築しました。

### 学んだ技術

- **NestJS**: モダンなNode.jsフレームワーク
- **Prisma**: 次世代ORM
- **PostgreSQL**: 信頼性の高いリレーショナルデータベース
- **Docker**: コンテナ化技術
- **Swagger**: API仕様書自動生成
- **TypeScript**: 型安全なJavaScript

### 実装した機能

✅ **完全なCRUD API**  
✅ **型安全な開発環境**  
✅ **入力バリデーション**  
✅ **エラーハンドリング**  
✅ **API仕様書の自動生成**  
✅ **データベース永続化**  

### コードの品質

- **Clean Code**: 読みやすく保守しやすいコード
- **SOLID原則**: 適切な責務分離
- **型安全性**: TypeScriptによる完全な型保護
- **テスタビリティ**: ユニット・E2Eテスト対応

このAPIを基盤として、React フロントエンド、認証機能、リアルタイム通信など、さらなる機能拡張が可能です。

### 参考リンク

- [NestJS 公式ドキュメント](https://nestjs.com/)
- [Prisma 公式ドキュメント](https://www.prisma.io/)
- [PostgreSQL 公式ドキュメント](https://www.postgresql.org/)

次回の **フロントエンド編** では、このAPIを使ってReact + TypeScriptでUIを構築していきます！

**Happy Coding! 🎉**