【NestJS 入門】TypeORM と PostgreSQL で API を作ってみよう

NestJSとPostgreSQLを使用してユーザー作成APIを作成します。

## 前提条件

以下のソフトウェアがインストールされていることを確認してください。

- Node.js (v16以上) がインストールされていること
- Docker と Docker Compose がインストールされていること
- テキストエディタ（VS Code等）が利用可能であること

## Docker と Docker Compose のインストール

### Windows の場合

1. [Docker Desktop for Windows](https://www.docker.com/products/docker-desktop/) をダウンロードします。
2. インストーラーを実行し、指示に従ってインストールを完了します。
3. インストール後、コンピューターを再起動します。
4. コマンドプロンプトまたはPowerShellで以下のコマンドを実行して、インストールを確認します：

```bash
docker --version
docker-compose --version
```

### macOS の場合

1. [Docker Desktop for Mac](https://www.docker.com/products/docker-desktop/) をダウンロードします。
2. ダウンロードした.dmgファイルを開き、DockerアプリケーションをApplicationsフォルダにドラッグします。
3. Applicationsフォルダからdockerを起動します。
4. ターミナルで以下のコマンドを実行して、インストールを確認します：

```bash
docker --version
docker-compose --version
```

## NestJSのインストールとプロジェクト作成

### NestJS CLIのインストール

グローバルにNestJS CLIをインストールします。

```bash
npm i -g @nestjs/cli
```

### 新しいプロジェクトの作成

プロジェクトを作成し、ディレクトリに移動します。

```bash
# プロジェクトを作成
nest new user-api-nestjs

# プロジェクトディレクトリに移動
cd user-api-nestjs
```

プロジェクト作成時にパッケージマネージャーを選択する画面が表示された場合は、`npm`を選択してください。

作成されたプロジェクトのフォルダ構造:

```txt
├── src
│   ├── app.controller.spec.ts
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   └── main.ts
├── test
│   ├── app.e2e-spec.ts
│   └── jest-e2e.json
├── .gitignore
├── .prettierrc
├── eslint.config.mjs
├── nest-cli.json
├── package-lock.json
├── package.json
├── README.md
├── tsconfig.build.json
└── tsconfig.json
```

上記を出力するコマンド:

```bash
git ls-files | tree --fromfile --dirsfirst
```

### 必要なパッケージのインストール

TypeORM、PostgreSQL、バリデーション等に必要なパッケージをインストールします。

```bash
npm install @nestjs/typeorm typeorm pg @nestjs/config class-validator class-transformer bcrypt
```

**インストールするパッケージの説明:**

- `@nestjs/typeorm`: NestJS用のTypeORM統合
- `typeorm`: TypeORM（Object-Relational Mapping）
- `pg`: PostgreSQL用ドライバー
- `@nestjs/config`: 環境変数管理
- `class-validator`: バリデーション
- `class-transformer`: オブジェクト変換
- `bcrypt`: パスワードハッシュ化

## Docker環境の構築

### docker-compose.ymlファイルの作成

プロジェクトのルートディレクトリに`docker-compose.yml`ファイルを作成します。

```bash
touch docker-compose.yml
```

`docker-compose.yml`に以下の内容を記述します：

```yaml
services:
  postgres:
    image: postgres:latest
    container_name: user-api-postgres
    ports:
      - "${POSTGRES_PORT:-5444}:5432"
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - user-api-network

  pgadmin:
    image: dpage/pgadmin4
    container_name: user-api-pgadmin
    ports:
      - "${PGADMIN_PORT:-5050}:80"
    environment:
      PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL}
      PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD}
    depends_on:
      - postgres
    networks:
      - user-api-network

networks:
  user-api-network:
    driver: bridge

volumes:
  postgres_data:
```

### 環境変数ファイル（.env）の作成

プロジェクトのルートディレクトリに環境変数ファイルを作成します。

```bash
touch .env
```

`.env`ファイルに以下の内容を記述します：

```env
# PostgreSQL設定
POSTGRES_HOST=localhost
POSTGRES_PORT=5444
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=user-api-db

# pgAdmin設定
PGADMIN_PORT=5050
PGADMIN_DEFAULT_EMAIL=admin@admin.com
PGADMIN_DEFAULT_PASSWORD=admin
```

**注意:** NestJSプロジェクト作成時に`.gitignore`ファイルには既に`.env`が含まれているため、追加する必要はありません。

### Dockerコンテナの起動

docker-composeを使用してPostgreSQLとpgAdminのコンテナを起動します。

```bash
docker-compose up -d
```

**コンテナの確認:**

```bash
docker-compose ps
```

正常に起動していれば、以下のようなステータスが表示されます：

```txt
NAME                IMAGE             COMMAND                  SERVICE    CREATED          STATUS         PORTS
user-api-pgadmin    dpage/pgadmin4    "/entrypoint.sh"         pgadmin    10 seconds ago   Up 9 seconds   443/tcp, 0.0.0.0:5050->80/tcp
user-api-postgres   postgres:latest   "docker-entrypoint.s…"   postgres   10 seconds ago   Up 9 seconds   0.0.0.0:5444->5432/tcp
```

## ユーザー作成APIの実装

### モジュール、コントローラー、サービスの生成

NestJS CLIを使用して必要なファイルを生成します。

```bash
# userモジュールを生成
nest generate module user

# userコントローラーを生成
nest generate --no-spec controller user

# userサービスを生成
nest generate --no-spec service user
```

### ディレクトリ構造の作成

エンティティとDTOのディレクトリを作成します。

```bash
# エンティティとDTOのディレクトリを作成
mkdir -p src/user/entities
mkdir -p src/user/dto
```

### User エンティティの作成

`src/user/entities/user.entity.ts`ファイルを作成します。

```bash
touch src/user/entities/user.entity.ts
```

以下の内容を記述します：

```typescript
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
import { Exclude } from 'class-transformer';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column({ unique: true })
  email: string;

  @Column()
  @Exclude({ toPlainOnly: true })
  password: string;

  @CreateDateColumn({ name: 'created_at' })
  createdAt: Date;

  @UpdateDateColumn({ name: 'updated_at' })
  updatedAt: Date;
}
```

### CreateUserDto の作成

`src/user/dto/create-user.dto.ts`ファイルを作成します。

```bash
touch src/user/dto/create-user.dto.ts
```

以下の内容を記述します：

```typescript
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsNotEmpty()
  @IsString()
  name: string;

  @IsNotEmpty()
  @IsEmail()
  email: string;

  @IsNotEmpty()
  @IsString()
  @MinLength(6)
  password: string;
}
```

### UserService の実装

生成された`src/user/user.service.ts`を以下の内容で置き換えます：

```typescript
import { Injectable, ConflictException, InternalServerErrorException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateUserDto } from './dto/create-user.dto';
import { User } from './entities/user.entity';
import * as bcrypt from 'bcrypt';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  async create(createUserDto: CreateUserDto): Promise<User> {
    try {
      // パスワードのハッシュ化
      const salt = await bcrypt.genSalt();
      const hashedPassword = await bcrypt.hash(createUserDto.password, salt);

      // ユーザーの作成
      const user = this.usersRepository.create({
        name: createUserDto.name,
        email: createUserDto.email,
        password: hashedPassword,
      });

      // データベースに保存
      return await this.usersRepository.save(user);
      // Excludeデコレータによりパスワードは自動的に除外される
    } catch (error) {
      // 一意制約違反（例：既存のメールアドレス）
      if (error.code === '23505') {
        throw new ConflictException('このメールアドレスは既に使用されています');
      } else {
        console.error('ユーザー作成エラー:', error);
        throw new InternalServerErrorException('ユーザーの作成中にエラーが発生しました');
      }
    }
  }
}
```

### UserController の実装

生成された`src/user/user.controller.ts`を以下の内容で置き換えます：

```typescript
import { Controller, Post, Body } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { User } from './entities/user.entity';

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Post()
  create(@Body() createUserDto: CreateUserDto): Promise<User> {
    return this.userService.create(createUserDto);
  }
}
```

### UserModule の更新

生成された`src/user/user.module.ts`を以下の内容で置き換えます：

```typescript
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { User } from './entities/user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}
```

### AppModule の設定

`src/app.module.ts`を以下の内容で置き換えます：

```typescript
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';
import { ValidationPipe } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
import { User } from './user/entities/user.entity';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        type: 'postgres',
        host: configService.get('POSTGRES_HOST'),
        port: +configService.get('POSTGRES_PORT'),
        username: configService.get('POSTGRES_USER'),
        password: configService.get('POSTGRES_PASSWORD'),
        database: configService.get('POSTGRES_DB'),
        entities: [User],
        synchronize: true, // 開発環境のみ
      }),
    }),
    UserModule,
  ],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: APP_PIPE,
      useClass: ValidationPipe,
    },
  ],
})
export class AppModule {}
```

### main.ts の更新

`src/main.ts`を以下の内容で置き換えます：

```typescript
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe, ClassSerializerInterceptor } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get('Reflector')));
  await app.listen(3000);
  console.log(`アプリケーションは http://localhost:3000 で実行中です`);
}

bootstrap();
```

## アプリケーションの起動

### Dockerコンテナの状態確認

まず、Dockerコンテナが正常に起動していることを確認します。

```bash
docker-compose ps
```

コンテナが起動していない場合は、以下のコマンドで起動します：

```bash
docker-compose up -d
```

### NestJSアプリケーションの起動

開発モードでNestJSアプリケーションを起動します。

```bash
npm run start:dev
```

**成功メッセージが表示されることを確認します:**

```log
アプリケーションは http://localhost:3000 で実行中です
```

## 動作確認

### API動作確認（curl使用）

ターミナルで以下のコマンドを実行してユーザー作成APIをテストします：

```bash
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "山田太郎", "email": "yamada@example.com", "password": "password123"}'
```

**期待されるレスポンス:**

```json
{
  "id": 1,
  "name": "山田太郎",
  "email": "yamada@example.com",
  "createdAt": "2025-05-25T...",
  "updatedAt": "2025-05-25T..."
}
```

### 複数のユーザーを作成してテスト

追加のユーザーを作成してAPIの動作を確認します。

```bash
# 2人目のユーザー
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "佐藤花子", "email": "sato@example.com", "password": "password456"}'

# 3人目のユーザー
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "田中次郎", "email": "tanaka@example.com", "password": "password789"}'
```

### バリデーションのテスト

バリデーション機能が正常に動作することを確認します。

**無効なメールアドレスでテスト:**

```bash
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "テスト", "email": "invalid-email", "password": "password123"}'
```

**短いパスワードでテスト:**

```bash
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "テスト", "email": "test@example.com", "password": "123"}'
```

**重複メールアドレスでテスト:**

```bash
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "重複テスト", "email": "yamada@example.com", "password": "password123"}'
```

### pgAdmin4でのデータベース確認

#### pgAdminへのアクセス

ブラウザで以下のURLにアクセスします：

```txt
http://localhost:5050
```

#### pgAdminへのログイン

設定した認証情報でログインします。

- **Email**: `admin@admin.com`
- **Password**: `admin`

#### PostgreSQLサーバーの追加

1. 左のサイドバーで「Add New Server」をクリックします。
2. **General**タブで設定を行います：
   - **Name**: `LocalPostgres`

3. **Connection**タブで設定を行います：
   - **Host name/address**: `postgres`（**重要**: IPアドレスではなくDockerサービス名を使用）
   - **Port**: `5432`（**重要**: 外部ポート5444ではなく、Docker内部ポート5432を使用）
   - **Maintenance database**: `user-api-db`
   - **Username**: `postgres`
   - **Password**: `postgres`

4. 「Save」をクリックして接続を保存します。

**注意点:**

- pgAdminはDockerコンテナ内で動作しているため、PostgreSQLコンテナにはDockerサービス名（`postgres`）でアクセスします。
- ポートは外部公開ポート（5444）ではなく、Docker内部ポート（5432）を使用します。

#### データの確認

1. 左のサイドバーで `LocalPostgres` > `Databases` > `user-api-db` > `Schemas` > `public` > `Tables` > `users` を展開します。
2. `users`テーブルを右クリックして「View/Edit Data」 > 「All Rows」を選択します。
3. 作成したユーザーデータが表示されることを確認します。

**確認すべき項目:**

- ユーザーIDが自動生成されている
- パスワードがハッシュ化されて保存されている
- `created_at`と`updated_at`のタイムスタンプが設定されている
- メールアドレスがユニークになっている

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

### コンテナが起動しない場合

ログを確認して問題を特定します。

```bash
# ログを確認
docker-compose logs postgres
docker-compose logs pgadmin

# コンテナを再起動
docker-compose down
docker-compose up -d
```

### データベース接続エラーの場合

以下の項目を確認します。

1. `.env`ファイルの設定を確認
2. PostgreSQLコンテナが正常に起動しているか確認
3. ポート5444が他のアプリケーションで使用されていないか確認

### NestJSアプリケーションが起動しない場合

以下の手順で問題を解決します。

1. 必要なパッケージがインストールされているか確認します：

   ```bash
   npm install
   ```

2. TypeScriptエラーがないか確認します：

   ```bash
   npm run build
   ```

## 完成したファイル構造

```txt
user-api-nestjs/
├── src
│   ├── user
│   │   ├── dto
│   │   │   └── create-user.dto.ts
│   │   ├── entities
│   │   │   └── user.entity.ts
│   │   ├── user.controller.ts
│   │   ├── user.module.ts
│   │   └── user.service.ts
│   ├── app.controller.spec.ts
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   └── main.ts
├── test
│   ├── app.e2e-spec.ts
│   └── jest-e2e.json
├── .env
├── .gitignore
├── .prettierrc
├── docker-compose.yml
├── eslint.config.mjs
├── nest-cli.json
├── package-lock.json
├── package.json
├── README.md
├── tsconfig.build.json
└── tsconfig.json
```