🤯

NestJSをTypeORM 0.3 で使う

2022/08/16に公開

NestJS と TypeORM の学習をしていたのですが、Web には TypeORM 0.2 系の情報が多く、0.3 系を使うと色々とはまったので、簡単な CRUD の REST API を作るまでをまとめました。

今回作成したコードは github にあります。
https://github.com/hasegawasatoshi/practice-nestjs-typeorm/tree/2022.08.16

PostgreSQL の構築

TypeORM は様々な DB に対応していますが、今回は PostgreSQL を使用します。docker-compose で起動できるようにします。

docker-compose.yml
version: '3'

services:
  postgres:
    image: postgres:14
    volumes:
      - ./postgres/data:/var/lib/postgresql/data
      - ./postgres/initdb:/docker-entrypoint-initdb.d
    ports:
      - "${POSTGRES_PORT}:5432"
    environment:
      - TZ=Asia/Tokyo
      - POSTGRES_DB=${POSTGRES_DB}
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}

volumes:
  postgres:
    driver: local

.env を作成して、設定情報を記載します。

.env
POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=5432
POSTGRES_DB=nestjs
POSTGRES_USER=postgres
POSTGRES_PASSWORD=changeme

ここまでで、いったん、PostgreSQL が起動することを確認します。

docker-compose up

アプリケーション作成

NestJS アプリを作成します。

nest new webapp
cd webapp

開発モードで起動します。

npm run start:dev

http://localhost:3000/ にアクセスし、Hello World! と表示されれば OK です。

TypeORM の導入

TypeORM に必要なパッケージをインストールします。

npm install @nestjs/typeorm typeorm reflect-metadata pg

PostgreSQL の設定情報を環境変数から読み込むために、 @nestjs/config と、バリデーションを行うための Joi をインストールします。

npm install @nestjs/config joi

0.2 では ormconfig.ts に接続オプションを記述していたようですが、0.3 では非推奨になったようです。
以下のように、モジュールを作成して、 app.module.ts でインポートします。

database.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
    imports: [
        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: [],
                synchronize: true,
            })
        }),
    ],
})
export class DatabaseModule { }

ここまでで、PostgreSQL と NestJS アプリを起動して動作確認してみます。

$ docker-compose up -d
$ cd webbapp
$ npm run start:dev
[1:12:06 PM] Starting compilation in watch mode...

[1:12:12 PM] Found 0 errors. Watching for file changes.

[Nest] 17171  - 08/16/2022, 1:12:16 PM     LOG [NestFactory] Starting Nest application...
[Nest] 17171  - 08/16/2022, 1:12:16 PM     LOG [InstanceLoader] DatabaseModule dependencies initialized +140ms
[Nest] 17171  - 08/16/2022, 1:12:16 PM     LOG [InstanceLoader] TypeOrmModule dependencies initialized +1ms
[Nest] 17171  - 08/16/2022, 1:12:16 PM     LOG [InstanceLoader] ConfigHostModule dependencies initialized +2ms
[Nest] 17171  - 08/16/2022, 1:12:16 PM     LOG [InstanceLoader] AppModule dependencies initialized +0ms
[Nest] 17171  - 08/16/2022, 1:12:16 PM     LOG [InstanceLoader] ConfigModule dependencies initialized +2ms
[Nest] 17171  - 08/16/2022, 1:12:16 PM     LOG [InstanceLoader] ConfigModule dependencies initialized +0ms
[Nest] 17171  - 08/16/2022, 1:12:17 PM     LOG [InstanceLoader] TypeOrmCoreModule dependencies initialized +934ms
[Nest] 17171  - 08/16/2022, 1:12:17 PM     LOG [RoutesResolver] AppController {/}: +8ms
[Nest] 17171  - 08/16/2022, 1:12:17 PM     LOG [RouterExplorer] Mapped {/, GET} route +4ms
[Nest] 17171  - 08/16/2022, 1:12:17 PM     LOG [NestApplication] Nest application successfully started +5ms

リソースの作成

CRUD generator でリソースのひな形を作成します。

$ nest g resource cats

? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? Yes
CREATE src/cats/cats.controller.spec.ts (556 bytes)
CREATE src/cats/cats.controller.ts (873 bytes)
CREATE src/cats/cats.module.ts (240 bytes)
CREATE src/cats/cats.service.spec.ts (446 bytes)
CREATE src/cats/cats.service.ts (595 bytes)
CREATE src/cats/dto/create-cat.dto.ts (29 bytes)
CREATE src/cats/dto/update-cat.dto.ts (165 bytes)
CREATE src/cats/entities/cat.entity.ts (20 bytes)
UPDATE package.json (2156 bytes)
UPDATE src/app.module.ts (788 bytes)
✔ Packages installed successfully.

Entity の作成

エンティティーの作成を行います。

src/cat/entities/cat.entity.ts
import {
    Column,
    CreateDateColumn,
    Entity,
    PrimaryGeneratedColumn,
    Timestamp,
    UpdateDateColumn,
} from 'typeorm';

@Entity('cats')
export class Cat {
    @PrimaryGeneratedColumn({
        name: 'id',
        unsigned: true,
        type: 'smallint',
        comment: 'ID',
    })
    readonly id: number;

    @Column('varchar', { comment: '猫の名前' })
    name: string;

    @CreateDateColumn({ comment: '登録日時' })
    readonly created_at?: Timestamp;

    @UpdateDateColumn({ comment: '最終更新日時' })
    readonly updated_at?: Timestamp;

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

database.modlue.tsCat を追加します。

database.modlue.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { Cat } from './cats/entities/cat.entity'; // <== 追加

@Module({
    imports: [
        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: [
                    // '**/entities/*.ts', // It cases "SyntaxError: Cannot use import statement outside a module"
                    Cat, // <== 追加
                ],
                synchronize: true,
            })
        }),
    ],
})
export class DatabaseModule { }

Database にテーブルが作られていることが確認できます。

Module の作成

src/cats/cats.module.ts
import { Module } from '@nestjs/common';
import { CatsService } from './cats.service';
import { CatsController } from './cats.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Cat } from './entities/cat.entity';

@Module({
  imports: [
    TypeOrmModule.forFeature([Cat]),
  ],
  exports: [TypeOrmModule],
  controllers: [CatsController],
  providers: [CatsService]
})
export class CatsModule { }

バリデーション

依存関係をインストールします。

npm i --save class-validator class-transformer

バリデーションクラスを実装していきます。

src/cats/dto/create-cat.dto.ts
import { IsNotEmpty, MaxLength } from 'class-validator';

export class CreateCatDto {
    @IsNotEmpty({ message: '名前は必須項目です' })
    @MaxLength(255, { message: '名前は255文字以内で入力してください' })
    name: string;
}
src/cats/dto/update-cat.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateCatDto } from './create-cat.dto';
import { IsNotEmpty, MaxLength } from 'class-validator';

export class UpdateCatDto extends PartialType(CreateCatDto) {
    @IsNotEmpty({ message: '名前は必須項目です' })
    @MaxLength(255, { message: '名前は255文字以内で入力してください' })
    name: string;
}

main.ts にバリデーションを適用する処理を追加します。

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(3000);
}
bootstrap();

Controller の実装

src/cats/cats.controller.ts
import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
} from '@nestjs/common';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';
import { UpdateCatDto } from './dto/update-cat.dto';
import { Cat } from './entities/cat.entity';
import { DeleteResult, UpdateResult } from 'typeorm';

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) { }

  @Post()
  async create(@Body() createCatDto: CreateCatDto): Promise<Cat> {
    return await this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return await this.catsService.findAll();
  }

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

  @Patch(':id')
  async update(
    @Param('id') id: string,
    @Body() updateCatDto: UpdateCatDto,
  ): Promise<Cat> {
    return await this.catsService.update(+id, updateCatDto);
  }

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

Service の実装

src/cats/cats.service.ts
import { Injectable, NotFoundException, InternalServerErrorException } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { UpdateCatDto } from './dto/update-cat.dto';
import { Cat } from './entities/cat.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { DeleteResult, Repository, UpdateResult } from 'typeorm';

@Injectable()
export class CatsService {
  constructor(@InjectRepository(Cat) private catRepository: Repository<Cat>) { }

  /**
   * @summary 登録機能
   * @param createCatDto
   */
  async create(createCatDto: CreateCatDto): Promise<Cat> {
    return await this.catRepository
      .save({ name: createCatDto.name })
      .catch((e) => {
        throw new InternalServerErrorException(e.message);
      });
  }

  /**
   * @summary 全件取得
   */
  async findAll(): Promise<Cat[]> {
    return await this.catRepository.find().catch((e) => {
      throw new InternalServerErrorException(e.message);
    });
  }

  /**
   * @summary 該当ID取得
   * @param id
   */
  async findOne(id: number): Promise<Cat> {
    const cat = await this.catRepository.findOneBy({ id });

    if (!cat) {
      throw new NotFoundException(
        `${id}に一致するデータが見つかりませんでした。`,
      );
    }
    return cat;
  }

  /**
   * @summary 該当ID更新
   * @param id
   */
  async update(id: number, updateCatDto: UpdateCatDto): Promise<Cat> {
    {
      await this.catRepository
        .update(id, { name: updateCatDto.name })
        .catch((e) => {
          throw new InternalServerErrorException(e.message);
        });
      const updatedPost = await this.catRepository.findOneBy({ id });
      if (updatedPost) {
        return updatedPost
      }
      throw new NotFoundException('${id}に一致するデータが見つかりませんでした。');
    }
  }

  /**
   * @summary 削除
   * @param id
   */
  async remove(id: number): Promise<DeleteResult> {
    const response = await this.catRepository.delete(id).catch((e) => {
      throw new InternalServerErrorException(e.message);
    });
    if (!response.affected) {
      throw new NotFoundException(`${id} was not found`);
    }
    return response;
  }
}

動作確認

登録

$ curl --location --request POST 'localhost:3000/cats' \
    --header 'Content-Type: application/json' \
    --data-raw '{"name": "tama"}'

全件取得

curl --location --request GET 'localhost:3000/cats' \
    --header 'Content-Type: application/json'

一件取得

curl --location --request GET 'localhost:3000/cats/1' \
    --header 'Content-Type: application/json'

更新

curl --location --request PATCH 'localhost:3000/cats/1' \
    --header 'Content-Type: application/json' \
    --data-raw '{"name": "pochi"}'

削除

curl --location --request DELETE 'localhost:3000/cats/1' \
    --header 'Content-Type: application/json'

参考文献

Discussion