NestJS + TypeORM 0.3でCRUD APIを構築する

NestJS V.9とTypeORM 0.3を使った最新のREST API構築方法を解説します。マイグレーション管理、バリデーション、CRUD操作の実装を含む実践的なガイドです。

#typescript #nestjs #typeorm #api

はじめに

NestJS V.9とTypeORM 0.3系を使った、最新のREST API構築方法を解説します。

GitHubリポジトリ: nestjs-demo-rest-api

環境

nest -v
# 9.1.4

# package.json
"@nestjs/typeorm": "^9.0.1"
"typeorm": "^0.3.10"

注意: Windows環境での動作確認は行っていません。

NestJSとは

NestJSは、効率的でスケーラブルなNode.jsサーバーサイドアプリケーションを構築するためのフレームワークです。

主な特徴:

  • TypeScriptを完全サポート
  • OOP(オブジェクト指向)、FP(関数型)、FRP(関数型リアクティブ)の要素を統合
  • Angular風のアーキテクチャ
  • 依存性注入(DI)によるテスタビリティの向上

プロジェクトセットアップ

NestJS CLIのインストール

# 作業ディレクトリを作成
mkdir nestjs-sample
cd nestjs-sample

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

# プロジェクトを作成(npmを選択)
nest new sample

# 依存関係をインストール
cd sample
npm install

ポート番号の変更(オプション)

デフォルトの3000番ポートを変更する場合:

// src/main.ts
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'

async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  await app.listen(3001) // ポート番号を変更
}
bootstrap()

アプリケーションの起動

npm run start:dev

# 以下のログが表示されればOK
# [Nest] Starting Nest application...
# [Nest] Nest application successfully started

動作確認:

curl http://localhost:3001
# "Hello World!" が返ればOK

CRUDリソースの生成

NestJS CLIを使って、CRUD操作の雛形を自動生成します。

# --no-specでテストファイルを生成しない
nest generate resource users --no-spec

# REST APIを選択
? What transport layer do you use? REST API

# CRUD entry pointsを有効化
? Would you like to generate CRUD entry points? Yes

# 以下のファイルが生成される
# src/users/users.controller.ts
# src/users/users.service.ts
# src/users/users.module.ts
# src/users/dto/create-user.dto.ts
# src/users/dto/update-user.dto.ts
# src/users/entities/user.entity.ts

公式ドキュメント: CRUD Generator

TypeORMのセットアップ

パッケージのインストール

npm install --save @nestjs/typeorm typeorm sqlite3

今回は開発環境の簡便さのためSQLiteを使用しますが、本番環境ではMySQL、PostgreSQLなどの使用を推奨します。

参考: NestJS Database

TypeORM設定ファイルの作成

⚠️ 警告:本番環境では環境変数から接続情報を取得してください。ハードコーディングは避けてください。

// typeOrm.config.ts
import { DataSource } from 'typeorm'

export default new DataSource({
  type: 'sqlite',
  database: 'data/dev.sqlite',
  entities: ['dist/**/entities/**/*.entity.js'],
  migrations: ['dist/**/migrations/**/*.js'],
  logging: true,
})

参考記事: API with NestJS - Database migrations with TypeORM

TypeORMコマンドの追加

// package.json
{
  "scripts": {
    "start:dev": "nest build && nest start --watch",
    "typeorm": "ts-node ./node_modules/typeorm/cli",
    "typeorm:run-migrations": "npm run typeorm migration:run -- -d ./typeOrm.config.ts",
    "typeorm:generate-migration": "npm run typeorm -- -d ./typeOrm.config.ts migration:generate ./migrations/$npm_config_name",
    "typeorm:create-migration": "npm run typeorm -- migration:create ./migrations/$npm_config_name",
    "typeorm:revert-migration": "npm run typeorm -- -d ./typeOrm.config.ts migration:revert"
  }
}

Windows環境の場合: $npm_config_name%npm_config_name%に変更してください。

AppModuleへの統合

// src/app.module.ts
import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { UsersModule } from './users/users.module'

@Module({
  imports: [
    UsersModule,
    TypeOrmModule.forRoot({
      type: 'sqlite',
      database: 'data/dev.sqlite',
      logging: true,
      entities: ['dist/**/entities/**/*.entity.js'],
      migrations: ['dist/**/migrations/**/*.js'],
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Entity定義

// src/users/entities/user.entity.ts
import { Column, PrimaryGeneratedColumn, Entity } from 'typeorm'

@Entity('users')
export class User {
  @PrimaryGeneratedColumn({ comment: 'アカウントID' })
  readonly id: number

  @Column('varchar', { comment: 'アカウント名' })
  name: string

  constructor(name: string) {
    this.name = name
  }
}

公式ドキュメント: NestJS Database - Entities

マイグレーション

マイグレーションファイルの生成

npm run typeorm:generate-migration --name=CreateUser

# Migration ./migrations/1665664827418-CreateUser.ts has been generated successfully.

マイグレーションの実行

npm run typeorm:run-migrations

# Migration CreateUser1665664827418 has been executed successfully.

マイグレーション実行後、data/dev.sqliteファイルが作成され、usersテーブルが生成されます。

バリデーションの実装

パッケージのインストール

npm install --save class-validator class-transformer

DTOへのバリデーション追加

// src/users/dto/create-user.dto.ts
import { IsNotEmpty, MaxLength } from 'class-validator'

export class CreateUserDto {
  @IsNotEmpty({ message: 'アカウント名は必須です' })
  @MaxLength(255, { message: 'アカウント名は255文字以内で入力してください' })
  name: string
}

グローバルバリデーションの有効化

// src/main.ts
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { ValidationPipe } from '@nestjs/common'

async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  app.useGlobalPipes(new ValidationPipe())
  await app.listen(3001)
}
bootstrap()

公式ドキュメント: NestJS Validation

CRUD機能の実装

UsersModuleへのTypeORM統合

// src/users/users.module.ts
import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'
import { UsersService } from './users.service'
import { UsersController } from './users.controller'
import { User } from './entities/user.entity'

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

Service実装

// src/users/users.service.ts
import { Injectable, InternalServerErrorException } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { User } from './entities/user.entity'
import { CreateUserDto } from './dto/create-user.dto'
import { UpdateUserDto } from './dto/update-user.dto'

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

  async create(createUserDto: CreateUserDto): Promise<{ message: string }> {
    await this.userRepository
      .save({ name: createUserDto.name })
      .catch((e) => {
        throw new InternalServerErrorException(
          `[${e.message}]アカウントの登録に失敗しました。`,
        )
      })

    return { message: 'アカウントの登録に成功しました' }
  }

  async findAll(): Promise<User[]> {
    return await this.userRepository.find()
  }

  async findOne(id: number): Promise<User> {
    return await this.userRepository.findOneBy({ id })
  }

  async update(id: number, updateUserDto: UpdateUserDto): Promise<{ message: string }> {
    await this.userRepository
      .update(id, { name: updateUserDto.name })
      .catch((e) => {
        throw new InternalServerErrorException(
          `[${e.message}]アカウントID「${id}」の更新に失敗しました。`,
        )
      })

    return { message: `アカウントID「${id}」の更新に成功しました。` }
  }

  async remove(id: number): Promise<{ message: string }> {
    await this.userRepository.delete(id).catch((e) => {
      throw new InternalServerErrorException(
        `[${e.message}]アカウントID「${id}」の削除に失敗しました。`,
      )
    })

    return { message: `アカウントID「${id}」の削除に成功しました。` }
  }
}

Controller実装

// src/users/users.controller.ts
import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
} from '@nestjs/common'
import { UsersService } from './users.service'
import { CreateUserDto } from './dto/create-user.dto'
import { UpdateUserDto } from './dto/update-user.dto'
import { User } from './entities/user.entity'

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  async create(@Body() createUserDto: CreateUserDto): Promise<{ message: string }> {
    return await this.usersService.create(createUserDto)
  }

  @Get()
  async findAll(): Promise<User[]> {
    return await this.usersService.findAll()
  }

  @Get(':id')
  async findOne(@Param('id') id: string): Promise<User> {
    return await this.usersService.findOne(+id)
  }

  @Patch(':id')
  async update(
    @Param('id') id: string,
    @Body() updateUserDto: UpdateUserDto,
  ): Promise<{ message: string }> {
    return await this.usersService.update(+id, updateUserDto)
  }

  @Delete(':id')
  async remove(@Param('id') id: string): Promise<{ message: string }> {
    return await this.usersService.remove(+id)
  }
}

APIのテスト

登録

curl -X POST -H "Content-Type:application/json" \
  http://localhost:3001/users \
  -d '{"name":"サンプル太郎"}'

# {"message":"アカウントの登録に成功しました"}

全件取得

curl http://localhost:3001/users

# [{"name":"サンプル太郎","id":1}]

更新

curl -X PATCH -H "Content-Type:application/json" \
  http://localhost:3001/users/1 \
  -d '{"name":"更新したよ"}'

# {"message":"アカウントID「1」の更新に成功しました。"}

個別取得

curl http://localhost:3001/users/1

# {"name":"更新したよ","id":1}

削除

curl -X DELETE http://localhost:3001/users/1

# {"message":"アカウントID「1」の削除に成功しました。"}

まとめ

NestJS V.9とTypeORM 0.3を使った、最新のREST API構築方法を解説しました。

実装したもの:

  • TypeORMを使ったEntity定義
  • マイグレーション管理
  • class-validatorを使ったバリデーション
  • CRUD操作の完全実装

重要なポイント:

  • TypeORM 0.3系では、Repository APIが大幅に変更されている
  • 本番環境では環境変数で設定を管理
  • グローバルバリデーションで一貫したエラーハンドリング
  • マイグレーションでスキーマをバージョン管理

この実装パターンは、スケーラブルなAPIの基礎として活用できます。