Yuki's Tech Blog

仕事で得た知見や勉強した技術を書きます。

NestJSにざっくり入門してみた(part2)

目次

概要

この記事では、NestJSのバリデーションと例外処理についてまとめました。

DTO(Data Transfer Object)とは

DTOとは、データの受け渡しに使われるオブジェクトのことです。 NestJSでは、リクエストのデータを受け取るときに主に使います。

DTOを使う3つのメリット

  1. メンテナンス性が高まる
    データの内容や型などが変更になった場合でも、修正箇所をDTO内に閉じ込めることができます。
  2. 安全性が高まる
    やりとりするデータをDTOの型に制限することができるので、誤ったデータが扱われるリスクが減ります。
  3. NestJSのバリデーション機能が使える
    型チェックだけでなく、複雑なバリデーションも可能です。NestJSのバリデーションは、型チェックだけではなく、入力の長さチェックなど、色々あります(これはRailsでも同じ)。

DTOを使ってcreateメソッドをリファクタリングしていく

DTOの作成方法

以下の手順でDTOを作成します。

  1. モジュールのディレクトリ直下にdtoディレクトリを作ります。
  2. 「メソッド名-単数形のモジュール名.dto」というファイル名のTSファイルを作ります。
  3. クラスでDTOを定義します(クラスバリデーターを使用するために、DTOはインターフェースではなく、クラスで定義します)。

例) create-item.dto.ts

import { Type } from 'class-transformer';
import { IsNotEmpty, IsInt, IsString, MaxLength, Min } from 'class-validator';

export class CreateItemDto {
  //プロパティはCreateリクエストのBodyパラメータと一緒
  // 以下のデコレータは、クラスバリデータから提供されるデコレータ
  @IsString()
  @IsNotEmpty()
  @MaxLength(40)
  name: string;

  @IsInt()
  @Min(1)
  @Type(() => Number)
  price: number;

  @IsString()
  @IsNotEmpty()
  description: string;
}

DTOを使うことで、@Bodyデコレータの記述をシンプルに書くことができます。

Before
↓ items.controller.ts

  // POSTメソッドのハンドラを作成する
  @Post()
  create(
    @Body('id') id: string,
    @Body('name') name: string,
    @Body('price') price: number,
    @Body('description') description: string,
  ): Item {
    const item = {
      id,
      name,
      price,
      description,
      status: ItemStatus.ON_SALE,
    };
    return this.itemsService.create(item);
  }

After
↓ items.controller.ts

  // POSTメソッドのハンドラを作成する
  // BodyパラメータとDTOのプロパティが等しい場合、これらをまとめて一つのボディデコレータで受け取ることができます。
 // DTOを使用する場合、Bodyデコレータに引数を与える必要はなく、DTO型の変数を定義すれば、その中に代入されます。 
  @Post()
  create(@Body() createItemDto: CreateItemDto): Item {
    return this.itemsService.create(createItemDto);
  }

↓ items.service.ts

  create(createItemDto: CreateItemDto): Item {
    const item = {
      ...createItemDto,
      status: ItemStatus.ON_SALE,
    };
    this.items.push(item);
    return item;
  }

NestJSにおけるバリデーションとは

NestJSにおけるバリデーションとは、リクエストオブジェクトの形式チェックを意味します。

ex)

  • ユーザー名は1文字以上20文字以内
  • emailはメールアドレスの形式
  • パスワードは英数字で8文字以上

NestJSでバリデーションを行う方法

NestJSでバリデーションを行う場合、Pipe(パイプ)という機能を使います。

Pipeとは

Pipeの主な役目は、ハンドラがリクエストを受け取る前に、リクエストに対して処理を行うことです。 Pipeを使うことで、リクエストデータの変換とバリデーションが可能になります。

変換やバリデーションの処理を行った後のデータをハンドラに渡します。また、処理の中で例外を返すことも可能です。

NestJSの組み込みパイプ

NestJSには、便利な組み込みパイプが用意されています。

  • ValidationPipe(バリデーションを行うためのバイプ)

↓ 変換のためのパイプ

  • ParseIntPipe(入力を整数型に変換)
  • ParseBoolPipe(入力をBoolean型に変換)
  • ParseUUIDPipe(入力をUUID型に変換)
  • DefaultValuePipe(入力がnull, undefinedの場合にデフォルト値を与える)

変換のパイプも変換ができなかった場合に例外を出すので、バリデーションとしても使えます。

Pipeをアプリケーションに適用させる3つの方法

以下の3つの方法でPipeをアプリケーションに適用させます。

方法1: ハンドラに適用する
ハンドラに@UsePipeデコレータをつけ、その引数に使用したいパイプを記述します。この方法は デコレータを適用したハンドラでのみ、パイプを使用できます。

@Post()
@UsePipes(ParseIntPipe)
create(@Body() createCatDto: CreateCatDto) {
  // 省略
}

方法2: パラメータごとに適用する
リクエストパラメータを取得するデコレータの第二引数に使用したいパイプを記述します。パラメータごとにパイプを使えるので、より柔軟に適応することができます。

  @Get(':id')
  findById(@Param('id', ParseUUIDPipe) id: string): Item {
    return this.itemsService.findById(id);
  }

方法3: グローバルに適用する
main.tsのapp.useGlobalPipesの引数に使用したいパイプのインスタンスを渡すことで、アプリケーション全体にそのパイプを適用させることができます。

↓ main.ts

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

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

クラスバリデータとは

クラスバリデータとは、入力値バリデーション用のライブラリです。ライブラリであるため、クラスバリデータを使用したい場合、インストールする必要があります。

DTOクラスのプロパティに、クラスバリデータから提供されるデコレータをつけることで、バリデーションのルールを定義します。

クラスバリデータでは、複数のエラーがあると、全て返してくれます。

クラスバリデータを使用してバリデーションを行う

クラスバリデータを使用してバリデーションを行うためには、ValidationPipeを設定する必要があります。全てのエンドポイントに対してバリデーションを有効にしたい場合は、方法3のやり方で ValidationPipe を設定します。

(注)入力チェックには class-validator 、型変換には class-transformerというライブラリを使用します。class-transformerも使う場合、忘れずにインストールするようにしましょう。

NestJSにおける例外処理

NestJSには、組み込みの例外クラスが用意されています。

例)

  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ConflictException

サービスの中に例外処理を書きます。
以下のコードでは、商品が見つからない場合に例外を返すような処理を書いてます。商品が見つからない場合、フロント側にはステータスコード404のレスポンスが返されます。

  findById(id: string): Item {
    const found = this.items.find((item) => item.id === id);
    if (found) {
      return found;
    } else {
      throw new NotFoundException();
    }
  }

終わり

NestJSで学んだ箇所はここまでなので、今回が最終回です。今後、NestJSの学習を再開するときは、同じタイトルで記事を書こうと思います。

参考記事

DTOとは - 意味をわかりやすく - IT用語辞典 e-Words

NestJS入門 TypeScriptではじめるサーバーサイド開発 | Udemy

Nest(TypeScript)で遊んでみる 〜Validation編〜 | AreaB Blog