🐈

【V.9対応】TypeScript +NestJSをプロジェクトで導入したら素晴らしかった件

2021/12/19に公開

はじめに

昨年12月に投稿させていただいた記事でしたがNestJSのバージョンアップに伴いTypeORM系の処理が動かなくなっていることを発見しましたので記事を更新させていただきました

https://qiita.com/advent-calendar/2021/timeleap-typescript

レポジトリはこちら

https://github.com/naoki-haba/nestjs-demo-rest-api

環境について

Windows環境での動作確認はしておりませんのでご了承ください

nest -v
9.1.4

"@nestjs/typeorm": "^9.0.1",
"typeorm": "^0.3.10"

NestJSとは?

https://nestjs.com/

https://github.com/nestjs/nest

Nest (NestJS) は、効率的でスケーラブルなNode.jsサーバサイドアプリケーションを構築するためのフレームワークです。プログレッシブJavaScriptを採用し、TypeScriptを完全にサポートし、OOP(オブジェクト指向プログラミング)、FP(機能的プログラミング)、FRP(機能的反応プログラミング)の要素を兼ね備えています。

環境構築

/nestjs-sample/sample
# NestJSをセットアップするディレクトリを作成します
mkdir nestjs-sample

# cliをインストールします
npm i -g @nestjs/cli

# プロジェクトを作成します
nest new sample

# 今回はnpmでインストールします
? Which package manager would you ❤️  to use? npm

cd sample

npm i

起動するポート番号を変更する

※ 3000番ポートを別のコンテナで利用しているため変更していますが、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();

Nestアプリケーションを起動する

/nestjs-sample/sample
npm run start:dev

[8:32:29 PM] Found 0 errors. Watching for file changes.

# 以下のログが出力されると成功しています
[Nest] 95887  - 10/13/2022, 8:32:31 PM     LOG [NestFactory] Starting Nest application...
[Nest] 95887  - 10/13/2022, 8:32:32 PM     LOG [InstanceLoader] TypeOrmModule dependencies initialized +710ms
[Nest] 95887  - 10/13/2022, 8:32:32 PM     LOG [InstanceLoader] AppModule dependencies initialized +0ms
[Nest] 95887  - 10/13/2022, 8:32:32 PM     LOG [InstanceLoader] TypeOrmCoreModule dependencies initialized +8ms
[Nest] 95887  - 10/13/2022, 8:32:32 PM     LOG [RoutesResolver] AppController {/}: +6ms
[Nest] 95887  - 10/13/2022, 8:32:32 PM     LOG [RouterExplorer] Mapped {/, GET} route +5ms
[Nest] 95887  - 10/13/2022, 8:32:32 PM     LOG [NestApplication] Nest application successfully started +3ms

CRUD構築用の雛形を作成する

NestJS CLIに便利なコマンドが用意されているので利用させていただきます

https://docs.nestjs.com/recipes/crud-generator#generating-a-new-resource

/nestjs-sample/sample
# 今回はテストは書かないので--no-specでテストファイルを作成しないようにしています
nest g resource users --no-spec
? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? Yes
CREATE src/users/users.controller.spec.ts (566 bytes)
CREATE src/users/users.controller.ts (894 bytes)
CREATE src/users/users.module.ts (247 bytes)
CREATE src/users/users.service.ts (609 bytes)
CREATE src/users/dto/create-user.dto.ts (30 bytes)
CREATE src/users/dto/update-user.dto.ts (169 bytes)
CREATE src/users/entities/user.entity.ts (21 bytes)
UPDATE src/app.module.ts (411 bytes)
✔ Packages installed successfully.

ORMのセットアップ

今回は公式がサンプルで使用しているTypeORMを利用します。
Prismaを利用する場合は読み替えてください。

https://docs.nestjs.com/techniques/database

/nestjs-sample/sample
npm i --save @nestjs/typeorm typeorm sqlite3

TypeORMの設定ファイルを作成する

設定について以下の記事を参考にさせていただきました

https://wanago.io/2022/07/25/api-nestjs-database-migrations-typeorm/

https://docs.nestjs.com/techniques/configuration

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,
});

package.jsonを更新する

TypeORMで利用するコマンドを追加してください。

Windows環境の場合は$npm_config_name%npm_config_name%としてください。

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"
  },

app.moduleTypeORMモジュールを読み込む

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

@Module({
  imports: [
    AppModule,
    // 環境変数から取得することを推奨します
    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を作成する

https://docs.nestjs.com/techniques/database

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-sample/sample
npm run typeorm:generate-migration --name=CreateUser

query: SELECT null as database, null as schema, * FROM "sqlite_master" WHERE "type" = 'table' AND "name" IN ('users')
query: SELECT null as database, null as schema, * FROM "sqlite_master" WHERE "type" = 'index' AND "tbl_name" IN ('users')
query: SELECT * FROM "sqlite_master" WHERE "type" = 'table' AND "name" = 'typeorm_metadata'
Migration /nestjs-demo-rest-api/sample/src/database/migrations/1665664827418-CreateUser.ts has been generated successfully.

マイグレーションの実行

/nestjs-sample/sample
npm run typeorm:run-migrations

query: SELECT * FROM "sqlite_master" WHERE "type" = 'table' AND "name" = 'migrations'
query: CREATE TABLE "migrations" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "timestamp" bigint NOT NULL, "name" varchar NOT NULL)
query: SELECT * FROM "migrations" "migrations" ORDER BY "id" DESC
0 migrations are already loaded in the database.
1 migrations were found in the source code.
1 migrations are new migrations must be executed.
query: BEGIN TRANSACTION
query: CREATE TABLE "users"
                             (
                                 "id"   integer PRIMARY KEY AUTOINCREMENT NOT NULL,
                                 "name" varchar NOT NULL
                             )
query: INSERT INTO "migrations"("timestamp", "name") VALUES (1665664827418, ?) -- PARAMETERS: ["CreateUser1665664827418"]
Migration CreateUser1665664827418 has been  executed successfully.
query: COMMIT

CRUDを実装する

usersモジュールにTypeORMを読み込む

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

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

登録したリポジトリをサービスで依存解決する

src/users/users.service.ts
export class UsersService {
  constructor(
    @InjectRepository(User) private userRepository: Repository<User>,
  ) {}
}

バリデーションを実装する

https://github.com/typestack/class-validator

https://docs.nestjs.com/techniques/validation

/nestjs-sample/sample
npm i --save class-validator class-transformer
src/users/dto/create-user.dto.ts
import { MaxLength } from 'class-validator';

export class CreateUserDto {
  @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();

サービスクラスを実装する

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

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

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

  /**
   * 一覧
   * @returns
   */
  async findAll(): Promise<User[]> {
    return await this.userRepository.find();
  }

  /**
   * 詳細
   * @param id
   * @returns
   */
  async findOne(id: number): Promise<User> {
    return await this.userRepository.findOneBy({
      id: id,
    });
  }

  /**
   * 更新
   * @param id
   * @param updateUserDto
   * @returns
   */
  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}」の更新に成功しました。` };
  }

  /**
   * 削除
   * @param id
   * @returns
   */
  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}」の削除に成功しました。` };
  }
}

サービスクラスをコントローラーから呼び出す

コントローラークラス
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) {}

  /**
   * 登録
   * @param createUserDto
   * @returns
   */
  @Post()
  async create(
    @Body() createUserDto: CreateUserDto,
  ): Promise<{ message: string }> {
    return await this.usersService.create(createUserDto);
  }

  /**
   * @summary 一覧
   * @returns
   */
  @Get()
  async findAll(): Promise<User[]> {
    return await this.usersService.findAll();
  }

  /**
   * @summary 詳細
   * @param id
   * @returns
   */
  @Get(':id')
  async findOne(@Param('id') id: string): Promise<User> {
    return await this.usersService.findOne(+id);
  }

  /**
   * @summary 更新
   * @param id
   * @param updateUserDto
   * @returns
   */
  @Patch(':id')
  async update(
    @Param('id') id: string,
    @Body() updateUserDto: UpdateUserDto,
  ): Promise<{ message: string }> {
    return await this.usersService.update(+id, updateUserDto);
  }

  /**
   * @summary 削除
   * @param id
   * @returns
   */
  @Delete(':id')
  async remove(@Param('id') id: string): Promise<{ message: string }> {
    return await this.usersService.remove(+id);
  }
}

RestAPIにリクエストを投げてみる

# 登録
curl -XPOST -H "Content-Type:application/json" localhost:3001/users -d '{"name":"サンプル太郎"}'

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

# 全件取得
curl -H "Content-Type:application/json" loca
lhost:3001/users

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

# 更新
curl -XPATCH -H "Content-Type:application/js
on" localhost:3001/users/1 -d '{"name":"更新したよ"}'

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

# 個別取得
curl -H "Content-Type:application/json" localhost:3001/users/1

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

# 削除
curl -XDELETE -H "Content-Type:application/j
son" localhost:3001/users/1

{"message":"アカウントID「1」の削除に成功しました。"}%  
GitHubで編集を提案

Discussion