ぽぴなび

知って感動した技術情報・生活情報や買ってよかったものの雑記です。

【NestJS】入力値のバリデーションを簡単に実装する

前回作成した Item API に入力値のバリデーションを作成していきます。

Item APIは以下のようになっています。

// items.controller.ts
@Controller('items')
export class ItemsController {
  constructor(private readonly itemsService: ItemsService) {}

  @Get()
  findAll() {
    return this.itemsService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.itemsService.findOne(id);
  }

  @Post()
  create(@Body() createItemDto: CreateItemDto) {
    return this.itemsService.create(createItemDto);
  }

  @Patch(':id')
  update(@Param('id') id: string, @Body() updateItemDto: UpdateItemDto) {
    return this.itemsService.update(id, updateItemDto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.itemsService.remove(id);
  }
}

ソースコードはこちら。

GitHub - popy1017/nest_with_auth0 at v1.3.0

NestJSのバリデーション

NestJSのバリデーションはPipeを使うことで簡単に実装することができます。
Pipeは入力値を特定の形式に変換したり、評価するために使用されるNestJSの機能です。
NestJSではバリデーションに便利なPipeがいくつか定義されているため、簡単に入力値のチェックを行うことができます。
詳しくはこちら:
Validation | NestJS - A progressive Node.js framework

準備

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

npm i --save class-validator class-transformer

Auto-validateの設定

main.tsに1行追記して、アプリレベルで設定をONにします。
これにより、すべてのエンドポイントで Auto-Validate が有効になります。

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe()); // <= 追加
  await app.listen(4000);
}
bootstrap();

リクエストボディ

Item APIでリクエストボディを指定することができるAPIPOST /itemsPATCH /itemsがあります。

// create-item.dto.ts
export class CreateItemDto {
  name: string;
  price: number;
}

// update-item.dto.ts
export class UpdateItemDto extends PartialType(CreateItemDto) {}

現状は特に何もチェックしていないため、例えばnameにnumber型の値を入れたり、priceにstring型の値を入れたりすることができてしまいます。

型のチェックをするためには、各プロパティにclass-validatorパッケージで提供されるデコレーターを付与します。(必要な作業はなんとこれだけです。)

import { IsNumber, IsString } from 'class-validator';

export class CreateItemDto {
  @IsString()
  name: string;

  @IsNumber()
  price: number;
}

実際にnameプロパティにnumberを設定しようとしてみるとちゃんとBad Requestが返ってくることがわかります。

curl --location --request POST 'http://localhost:4000/items' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": 1234,
    "price": 4000
}'

{
    "statusCode": 400,
    "message": [
        "name must be a string"
    ],
    "error": "Bad Request"
}

他にも、必須プロパティをチェックする@IsNotEmptyや文字数をチェックする@MaxLength@MinLengthなど、様々なデコーレータが用意されているのでありがたいです。
最終的には以下のようにしました。

export class CreateItemDto {
  // 3~10文字の文字列を許可
  @IsNotEmpty()
  @IsString()
  @MinLength(3)
  @MaxLength(10)
  name: string;

  // 正の数値のみ許可
  @IsNotEmpty()
  @IsPositive()
  price: number;
}

不要なkeyを削除する

上記で必要なパラメータのチェックはできましたが、不要なパラメータを受け取った際の挙動を見てみると、ちゃっかり受け取ってしまっていることがわかります。

  @Post()
  create(@Body() createItemDto: CreateItemDto) {
    console.log(createItemDto);
    return this.itemsService.create(createItemDto);
  }

// output
{ name: '123', price: 4000, hoge: '123' }

不要なパラメータを受け取りたくない場合は、最初に設定したValidationPipewhitelistオプションを有効にします。
これで、バリデーションデコレーターが付与されていないプロパティは削除されるようになりました。

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true, // <= 追加
    }),
  );
  await app.listen(4000);
}
bootstrap();

また、より厳格にチェックしたい場合は(whitelistオプションと)forbidNonWhitelistedオプションを有効にすることで不要なパラメータがあった時点でエラーレスポンスを返すこともできます。

{
    "statusCode": 400,
    "message": [
        "property hoge should not exist"
    ],
    "error": "Bad Request"
}

NestJS、、、すごい。。。

おまけ

パスパラメータやクエリパラメータも同様にチェックできます。

パスパラメータ

// find-one-params.ts 
export class FindOneParams {
  @IsUUID()
  id: string;
}

// items.controller.ts
~~
@Patch(':id')
update(@Param() params: FindOneParams, @Body() updateItemDto: UpdateItemDto) {
  return this.itemsService.update(params.id, updateItemDto);
}
~~

クエリパラメータ

クエリパラメータの場合は若干注意が必要で、すべてstringになってしまうので@Type(() => Number)等で明示的に変換する必要があるようです。
Validating numeric query parameters in NestJS - DEV Community 👩‍💻👨‍💻

// find-all-params.ts
enum SortKey {
  NAME = 'name',
  PRICE = 'price',
}

export class FindAllParams {
  @IsOptional()
  @Type(() => Number)
  @IsPositive()
  count: number;

  @IsOptional()
  @IsString()
  @IsEnum(SortKey)
  sortkey: SortKey;
}

// items.controller.ts
~~
@Get()
findAll(@Query() query: FindAllParams) {
  return this.itemsService.findAll();
}
~~

また、パラメータの数が少ない場合等は以下のようにしてもチェックや変換ができます。

Validation | NestJS - A progressive Node.js framework

@Get(':id')
findOne(
  @Param('id', ParseIntPipe) id: number,
  @Query('sort', ParseBoolPipe) sort: boolean,
) {
  console.log(typeof id === 'number'); // true
  console.log(typeof sort === 'boolean'); // true
  return 'This action returns a user';
}