TypeScript +NestJSをプロジェクトで導入したら素晴らしかった件

# はじめに

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

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


レポジトリはこちら

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

# 環境について

 `Windows`環境での動作確認はしておりませんのでご了承ください
 
 ```bash
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（機能的反応プログラミング）の要素を兼ね備えています。

# 環境構築

```bash:/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番ポートが空いている場合はこちらの手順は不要です

```ts: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アプリケーションを起動する

```bash:/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

```bash:/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

```bash:/nestjs-sample/sample
npm i --save @nestjs/typeorm typeorm sqlite3
```

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

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

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

:::message alert
接続情報については環境変数から取得することを推奨されていますのでハードコーディングは避けてください。
今回はデモ用のハンズオンということで直書きしています。
:::

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

```ts: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%`としてください。

```json: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.module`で`TypeORM`モジュールを読み込む

```ts: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

```ts: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;
  }
}
```


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

```bash:/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.
```

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

```bash:/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`を読み込む

```ts: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 {}
```

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

```ts: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

```bash:/nestjs-sample/sample
npm i --save class-validator class-transformer
```

```ts:src/users/dto/create-user.dto.ts
import { MaxLength } from 'class-validator';

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

## バリデーションをすべてのエンドポイントで有効化する

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



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

:::details サービスクラス
```ts: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}」の削除に成功しました。` };
  }
}
```
:::

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

:::details コントローラークラス
```ts: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`にリクエストを投げてみる

```bash
# 登録
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」の削除に成功しました。"}%  
```