前回作成した 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でリクエストボディを指定することができるAPIはPOST /items
とPATCH /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' }
不要なパラメータを受け取りたくない場合は、最初に設定したValidationPipe
のwhitelist
オプションを有効にします。
これで、バリデーションデコレーターが付与されていないプロパティは削除されるようになりました。
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'; }