🛠️

[NestJSハンズオン] Pipeを使って変換、検証を実装する

2022/12/28に公開

こんにちは、株式会社スペースマーケットでバックエンドエンジニアをしているtchmrです。
弊社では近年、RailsからNestJSへのAPIリプレイスを進めています。

いよいよ自分もキャッチアップしていかなければ!となり学んだことをアウトプットしていきたいと思います。

今回のテーマはPipeです。

NestJSにおけるPipeとは

入力 → Pipe処理 → ルートハンドラ処理 → 出力

クライアントから渡ってきた入力値を使ってルートハンドラの処理が実行されますが、これの実行前にPipeの処理が差し込まれます。

上記のような特徴からPipeには2つの主なユースケースがあります。

  • 変換:入力値を特定の型に変換する
  • 検証:入力値を評価し、正しければ実行を継続し、間違っていれば例外を投げる

以下では、NestJS公式が提供しているサンプルコードを用いてPipeの挙動を確認していきたいと思います。
https://github.com/nestjs/nest/tree/master/sample/01-cats-app

変換

以下の例では、パラメータで渡ってきた文字列の"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を作成することも可能なので実現できることの幅は広く非常に便利かと思います。

スペースマーケットでは、一緒にサービスを成長させていく仲間を探しています。
サービスに興味がある、技術的なチャレンジをしつつ事業を成長させてみたい方など、まずはカジュアルにお話できれば嬉しいです。

https://www.wantedly.com/projects/1113544

https://www.wantedly.com/projects/1113570

https://www.wantedly.com/projects/1061116

スペースマーケット Engineer Blog

Discussion