[NestJSハンズオン] Pipeを使って変換、検証を実装する
こんにちは、株式会社スペースマーケットでバックエンドエンジニアをしているtchmrです。
弊社では近年、RailsからNestJSへのAPIリプレイスを進めています。
いよいよ自分もキャッチアップしていかなければ!となり学んだことをアウトプットしていきたいと思います。
今回のテーマはPipe
です。
NestJSにおけるPipeとは
入力 → Pipe処理 → ルートハンドラ処理 → 出力
クライアントから渡ってきた入力値を使ってルートハンドラの処理が実行されますが、これの実行前にPipeの処理が差し込まれます。
上記のような特徴からPipeには2つの主なユースケースがあります。
- 変換:入力値を特定の型に変換する
- 検証:入力値を評価し、正しければ実行を継続し、間違っていれば例外を投げる
以下では、NestJS公式が提供しているサンプルコードを用いてPipeの挙動を確認していきたいと思います。
変換
以下の例では、パラメータで渡ってきた文字列の"1"を整数の1に変換した上でfindOne
の引数として使用しています。
sample/01-cats-app/src/cats/cats.controller.ts
import { Controller, Get, Param } from '@nestjs/common';
import { ParseIntPipe } from '@nestjs/common';
import { CatsService } from './cats.service';
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Get(':id')
// @Paramsのidを文字列から整数に変換
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
}
sample/01-cats-app/src/cats/cats.service.ts
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
@Injectable()
export class CatsService {
// 便宜的にメモリ上に定義
private readonly cats: Cat[] = [
{
id: 1,
name: 'リリー',
age: 1,
breed: 'スコティッシュ・フォールド',
},
{
id: 2,
name: 'アムール',
age: 3,
breed: 'マンチカン',
}
];
findOne(id: number): Cat[] {
return this.cats.filter(cat => cat.id === id);
}
}
リクエスト
GET localhost:3000/cats/1
レスポンス
{
"data": [
{
"id": 1,
"name": "リリー",
"age": 1,
"breed": "スコティッシュ・フォールド"
}
]
}
動作確認のためにParseIntPipe
を外してみましょう。
文字列の"1"のままfindOne
を実行するため対象のデータを取得することができません。
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Get(':id')
// ParseIntPipeを削除
async findOne(@Param('id') id: number) {
return this.catsService.findOne(id);
}
}
リクエスト
GET localhost:3000/cats/1
レスポンス
{
"data": []
}
検証
強力な検証ライブラリとしてclass-validator
があります。
以下ではこれを用いて検証を実装していきます。
※ 内部でclass-transformer
を使用しているためこちらも併せてもインストールしておく必要があります。
Cat作成リクエストの入力値を検証する実装をしていきたいと思います。
sample/01-cats-app/src/cats/cats.controller.ts
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
}
sample/01-cats-app/src/cats/dto/create-cat.dto.ts
CreateCatDtoの各プロパティにバリデーション用のデコレータを付与します。
import { IsInt, IsString } from 'class-validator';
export class CreateCatDto {
// 文字列検証
@IsString()
readonly name: string;
// 整数検証
@IsInt()
readonly age: number;
@IsString()
readonly breed: string;
}
sample/01-cats-app/src/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);
// 以下を記述。ValidationPipeは@nestjs/commonから読み込んでいる
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();
リクエスト
POST localhost:3000/cats/
Body
{
"name": 12345,
"age": 3,
"breed": "三毛猫"
}
レスポンス
{
"statusCode": 400,
"message": [
"name must be a string"
],
"error": "Bad Request"
}
nameがstringではないので400エラーで弾けていることを確認できました。
それでは、挙動確認としてValidationPipeの記述をコメントアウトしてみます。
sample/01-cats-app/src/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);
console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();
リクエスト
POST localhost:3000/cats/
Body
{
"name": 12345,
"age": 3,
"breed": "三毛猫"
}
リクエスト
GET POST localhost:3000/cats/
レスポンス
バリデーションが実行されず不正なデータが追加されています。
{
"data": [
{
"id": 1,
"name": "リリー",
"age": 1,
"breed": "スコティッシュ・フォールド"
},
{
"id": 2,
"name": "アムール",
"age": 3,
"breed": "マンチカン"
},
{
"id": 3,
"name": 12345, // バリデーションが効かなかったためnameが整数で登録される
"age": 3,
"breed": "三毛猫"
}
]
}
まとめ
Pipeを使用することで入力値を加工したり不正なデータを事前に弾いたりすることができます。
他にも多くの組み込みPipeがありますし、独自にカスタムPipeを作成することも可能なので実現できることの幅は広く非常に便利かと思います。
スペースマーケットでは、一緒にサービスを成長させていく仲間を探しています。
サービスに興味がある、技術的なチャレンジをしつつ事業を成長させてみたい方など、まずはカジュアルにお話できれば嬉しいです。
スペースを簡単に貸し借りできるサービス「スペースマーケット」のエンジニアによる公式ブログです。 弊社採用技術スタックはこちら -> whatweuse.dev/company/spacemarket
Discussion