Pipeとは

Pipeとは

NestJSのPipeは、@Injectable()デコレータでアノテーションされたクラスで、PipeTransformインターフェイスを実装しています。
Pipeには2つの使い方があります。
  • 変換:入力データを希望の型に変換する(例:stringnumber)
  • 検証:入力データを評価し、データが正しければ実行を継続し、間違っていれば例外を投げる
いずれもPipeはControllerのルートハンドラによって処理されるargumentsを操作します。
NestJSはメソッドが呼び出される直前にPipeを挿入し、Pipeはメソッドの引数を受け取って処理します。変換や検証はその時点で実施され、最後に変換された引数でルートハンドラが呼び出されます。
NestJSには組み込みのPipeが多数用意されており、すぐに使えます。独自のカスタムPipeを実装できます。本ページでは、組み込みPipeを紹介し、ルートハンドラにバインドする方法を示します。次に、いくつかカスタムパイプを見て、一からパイプを作る方法を簡潔に示します。
メモ Pipe は例外領域の内部で実行されます。つまり、Pipe が例外を投げた場合は、例外レイヤー (グローバル例外フィルタと、現在のコンテキストに適用される例外フィルタ) によって処理されます。
このことから、Pipe で例外が発生した場合、その後にコントローラのメソッドが実行されません。これにより、外部ソースからアプリケーションに取り込まれたデータをシステム境界で検証するためのベストプラクティスの手法を得ることができます。

組み込みPipe

NestJSでは以下の6つのPipeがすぐに使えます。
  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • DefaultValuePipe
これらはすべて@nestjs/commonパッケージからエクスポートされます。
ParseIntPipeとは、変換パイプのユースケースで。Pipeはメソッドハンドラの変数がJavaScriptの整数に変換されることを保証します。変換に失敗すれば例外を投げます。
本ページの後半では、ParseIntPipeのシンプルなカスタム例を紹介します。下記のテクニックの例は、本章でParseIntPipeの簡単なカスタム実装を紹介します。
以下のテクニックの例は、他の組み込みPipeにも適用されます。

Pipeをバインドする

Pipeを使用するには、Pipeクラスのインスタンスを適切なコンテキストにバインドする必要があります。ParseIntPipeの例では、パイプを特定のルートハンドラメソッドに関連付け、そのメソッドが呼ばれる前にPipeが実行されるようにします。
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
これはfindOne()メソッドで受け取るパラメータが数字であるか(this.catsService.findOne()の呼び出しで予想された通り)、ルートハンドラが呼び出される前に例外が投げられるかのどちらかであることを保証しています。
例えば、レスポンスが以下のように投げられたとします。
GET localhost:3000/abc
NestJSは以下のように例外を投げます。
{
"statusCode": 400,
"message": "Validation failed (numeric string is expected)",
"error": "Bad Request"
}
この例外により、findOne()メソッドの本体が実行されなくなります。
上述の例では、インスタンスではなくクラス(ParseIntPipe)を渡しており、インスタンス化の責任をフレームワークに委ねて、依存性注入を可能にしています。PipeやGuardと同様に、代わりにインプレースインスタンスを渡すこともできます。お決まりのインプレースインスタンスを渡すのは、以下のように補足を渡して組み込みPipeの動作をカスタマイズしたい場合に便利です。
@Get(':id')
async findOne(
@Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
id: number,
) {
return this.catsService.findOne(id);
}
変換機能を持つ他のPipeをバインドしても、同様に動作します。これらのPipeはすべてルート変数、クエリ文字列変数、リクエストの本文の情報の値を検証するコンテキストで動作します。
例えば、クエリ文字列変数の場合
@Get()
async findOne(@Query('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
ParseUUIDPipeを使って文字列パラメータを解析し、UUIDかどうかを検証する例を示します。
@Get(':uuid')
async findOne(@Param('uuid', new ParseUUIDPipe()) uuid: string) {
return this.catsService.findOne(uuid);
}
メモ ParseUUIDPipe()を使用すると、バージョン1、4あるいは5のUUIDをパースすることになります。
上記では、さまざまなParse*系の組み込みPipeをバインドする例を見てきました。検証用Pipeのバインディングは少し違います。

カスタムPipe

前述のとおり、カスタムPipeは自分で構築できます。NestJSには堅牢な組み込みのParseIntPipeValidationPipeがありますが、カスタムPipeがどのように構築されるかを見るために、それぞれの簡単なカスタムバージョンをゼロから構築してみましょう。
validation.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value;
}
}
メモ PipeTransform<T, R>は任意のPipeが実装しなければならないインターフェイスです。この汎用インターフェイスでは、入力値の値を示すためにTを使用し、transform()メソッドの戻り値の型を示すためにRを使います。
すべてのPipeは、PipeTransformインターフェイスの契約を満たすためにtransform()メソッドを実装する必要があります。このメソッドには2つのパラメータが存在します。
  • value
  • metadata
valueパラメータは現在処理中のメソッド引数(ルート処理メソッドが受け取る前)であり、metadataが現在処理中のメソッド引数のメタデータです。metadataオブジェクトは次のようなプロパティを持ちます。
export interface ArgumentMetadata {
type: 'body' | 'query' | 'param' | 'custom';
metatype?: Type<unknown>;
data?: string;
}
これらのプロパティは、現在処理中の引数を記述します。

type

引数が@Body()@Query()@Param()やカスタムパラメータのいずれかであることを示します。

metatype

引数のメタタイプ(例:String)を示します。ルートハンドラのメソッドシグネチャで型宣言を省略した場合、あるいはvanillaJSを使った場合、この値は未定義になります。

data

@Body('string')のように、デコレータに渡す文字列を指定します。デコレータの()を空にしておくと未定義になります。
注意 TypeScriptのインターフェイスはトランスパイル時に消滅します。したがって、メソッドパラメータの型がクラスではなくインターフェイスとして宣言されている場合、メタタイプの値はObjectになります。

スキーマに基づいた検証

検証Pipeをもう少し便利にしてみましょう。CatsControllercreate()メソッドに注目します。ここで、来たポストの本体であるオブジェクトが有効かどうか、サービスメソッドが起動する前に確かめられることを確認します。
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}yy
createDtoの本体により深く注目していきます。型はCreateCatDtoとなります。
create-cat.dto.ts
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
create()メソッドに入ってくるリクエストに有効な本文が含まれていることを確認します。その為に、createCatDtoオブジェクトが3つのメンバを確認する必要がある。ルートハンドラメソッドの中で行うことはできるが、単一責任の原則を破る理想的な方法ではありません。
検証用クラスを作成してタスクを以上スルという方法もあります。それぞれのメソッドの最初にこのバリデータを呼び出さなければならないデメリットがあります。
バリデーション用Middlewareを作る方法はどうでしょうか。動く可能性がないとはいえませんが、残念ながらアプリ全体のすべてのコンテキストで使える汎用的なMiddlewareは作れません。Middlewareは呼び出されるハンドラやそのパラメータを含む実行コンテキストを認識していないからです。
ここで検証用Pipeが求められます。

オブジェクトスキーマの検証

オブジェクトの検証をクリーンでDRY(Don't Repeat Yourself)な方法で実施するには、いくつかのアプローチがあります。一般的なアプローチの一つはスキーマベースの検証を行うことです。実際に試してみましょう。
joiライブラリを使うと、読みやすいAPIを用いてスキーマを作成できます。joiベースのスキーマを使った検証Pipeを作ってみましょう。
まずは、以下のコマンドを入力してください。
npm install --save joi
npm install --save-dev @types/joi
以下のサンプルコードは、constructorの引数としてスキーマを受け取るシンプルなクラスを実装したものです。次に、schema.validate()メソッドを適用し、指定したスキーマに対して入力した引数を検証します。
先述通り、検証Pipeは値をそのまま帰すか例外を投げます。次のセクションでは、@UsePipes()デコレータを使って指定したControllerメソッドに適切なスキーマを提供する方法を見ていきます。そうすれば、検証Pipeをコンテキスト間で再利用できます。
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ObjectSchema } from 'joi';
@Injectable()
export class JoiValidationPipe implements PipeTransform {
constructor(private schema: ObjectSchema) {}
transform(value: any, metadata: ArgumentMetadata) {
const { error } = this.schema.validate(value);
if (error) {
throw new BadRequestException('Validation failed');
}
return value;
}
}

検証Pipeのバインディング

上記で変換Pipe(ParseIntPipeParse*パイプ)をバインドする方法を確認しました。
検証Pipeのバインドの方法は非常に簡単です。今回は、メソッド呼び出しの段階でPipeをバインドしたいです。
現在の例では、JoiValidationPipeを使うために以下のことを行う必要があります。
  1. 1.
    JoiValidationのインスタンスを作る
  2. 2.
    Pipeのクラスコンストラクタにコンテキスト固有のjoiスキーマを渡す
  3. 3.
    Pipeをメソッドにバインドする
以下に示す通り、@UsePipe()を使って進めます。
@Post()
@UsePipes(new JoiValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
メモ @UsePipes()デコレータは@nestjs/commonからインポートされています。

クラスバリデータ

注意 このセクションのテクニックは必須です。(NestJSではTypeScriptで操作されるので)
このセクションでは、バリデーションテクニックの代替となる実装を見ていきます。NestJSはclass-validatorライブラリとの相性が良いです。
この強力なライブラリを使えば、デコレータベースの検証を行えます。この手法はとても強力で、特にNestJSのPipe機能と組み合わせると、処理中のプロパティのメタタイプにアクセスできるので非常に便利です。
まずは以下のコマンドを叩いてパッケージをインストールしてください。
npm i --save class-validator class-transformer
インストールすると、CreateCatDtoクラスにいくつかのデコレータを追加できます。この手法には大きなメリットがあります。それは、CreateCatDtoクラスはポストのbodyオブジェクトに対し、複数のバリデーションクラスを実装する必要がないことです。
create-cat.dto.ts
import { IsString, IsInt } from 'class-validator';
export class CreateCatDto {
@IsString()
name: string;
@IsInt()
age: number;
@IsString()
breed: string;
}
これらのアノテーションを使うvalidationPipeクラスができました。
validation.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToClass(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException('Validation failed');
}
return value;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}
上記のサンプルコードを見ていきましょう。最初に、transform()メソッドがasyncと設定されていることに留意する必要があります。これは、NestJSが動機と非同期両方のPipeをサポートしているからできることです。
今回asyncと設定したのは、class-validatorによる検証のうちいくつかが非同期にできるためです。
次に、メタタイプフィールドをメタタイプ変数として抽出する(ArgumentMetadataからこのメンバだけを抽出する)ために分割代入を使っていることに注意します。これは、「すべてのArgumentMetadataを取得してからメタタイプ変数を割り当てるための追加の文を持つ」動作を表現する構文に過ぎません。
また次に、ヘルパー関数toValidate()に注目しましょう。これは、現在処理中の引数がネイティブのJavaScript型である場合に検証ステップをバイパスする役割を果たします(そういった引数には検証用のデコレータをつけられないので、検証ステップを実行する理由がない)。
さらに、次にクラス変換関数plainToClass()を使ってプレーンなJavaScriptの引数オブジェクトを型付きのオブジェクトに変換し、検証を適用できるようにします。なぜならば、ネットワークのリクエストがデシリアライズされた場合、受信したpostのbobyオブジェクトはあらゆる型情報を持たないからです。
クラスバリデータは先程DTO用に定義したバリデーションデコレータを使用する必要があるため、この変換を実行して受信したbodyを適切にデコレーションされたオブジェクトとして扱う必要があります。
最後に、先述の通り、これは検証Pipeであるため、値を変更せずに返すか例外を投げます。
最後のステップは検証Pipeをバインドすることです。パラメータ、メソッド、コントローラやグローバルの単位でスコープ化できます。さきほど、joiベースの検証用PipeではメソッドレベルでPipeをバインドする例を見ました。以下の例では、Pipeのインスタンスをルートハンドラの@Body()デコレータにバインドして、投稿のbody部分を検証するためにPipeが呼び出されるようにします。
cats.controller.ts
@Post()
async create(
@Body(new ValidationPipe()) createCatDto: CreateCatDto,
) {
this.catsService.create(createCatDto);
}
パラメータスコープ付きのPipeは、検証ロジックがある特定のパラメータにのみ関係する場合に便利です。

グローバルスコープ化Pipe

ValidationPipeは可能な限り汎用的に作成されています。アプリ全体のすべてのルートハンドラに適用されるように、グローバルスコープを付けて設定することで実用性を最大限にできます。
main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
メモ ハイブリッドアプリの場合、useGlobalPipes()メソッドはゲートウェイとマイクロサービス用のPipeを作りません。「標準的な」マイクロサービスアプリの場合、useGlobalPipes()はグローバルにPipeをマウントします。
グローバルPipeはすべてのControllerとすべてのルートハンドラに対して、アプリケーション全体で適用されます。
依存関係という点では、(上記の例のようにuseGlobalPipes()を活用して)任意のModuleの外部から登録されたグローバルPipeは、バインディングが任意のModuleのコンテキストの外で行われています。したがって、依存関係をインジェクションできないことには十分注意してください。
この問題を解決するために、以下のような構成で任意のModuleから直接グローバルPipeを作成します。
app.module.ts
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}
メモ
このアプローチを使用してPipeの依存性インジェクションを行う際、この構造が採用されているModuleに関係なくPipeはグローバルであることには十分に注意してください。
やるならPipeが定義されているModule(上記のプログラムではValidationPipe)を選びましょう。また、カスタムプロバイダの登録を扱う方法はuseClassだけではありません。

変換Pipeの使用例

カスタムPipeの使用例は検証だけではありません。Pipeは入力データを任意の形式に変換できます。これは、transform関数から返される値が引数の前の値を完全に上書きするからです。
変換Pipeはどんなときに便利でしょうか?クライアントから渡されたデータがルートハンドラメソッドで適切に処理される前に、データの変更(例えば文字列を整数に変換するとか)が必要な場合を考えてみましょう。さらに言えば、いくつかの必須データフィールドが欠落していて、デフォルト値を適用したい場合もあります。変換Pipeは、クライアントリクエストとリクエストハンドラ間に処理関数を介在させることで、これらの機能を実現できます。
文字列を整数値にパースするためのシンプルなParseIntPipeを提示します。(※前述の通り、NestJSにはより洗練されたParseIntPipeが組み込まれています。カスタム変換Pipeの単純な例として組み込みます)
parse-int.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Validation failed');
}
return val;
}
}
このPipeを、選ばれたparamにバインドします。
@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
return this.catsService.findOne(id);
}
もう一つの有用な変換の例は、リクエストで提供されたidを使ってデータベースから既存のユーザのエンティティを選択することです。
@Get(':id')
findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
return userEntity;
}
このPipeの実装は読者に委ねますが、他のすべての変換Pipeと同様に、入力値(id)を受け取って出力値(UserEntityオブジェクト)を返すことには注意してください。
これで、お決まりのコードをハンドラから共通のPipeに抽象化してコードをより宣言的でDRYなものにできます。

デフォルトの提供

Parse*Pipeはパラメータの値が定義されていることを期待します。nullundefinedを受け取った時に例外を投げます。エンドポイントが失われたquerystringパラメータの値を処理できるようにするためにはParse*Pipeがこれらの値を処理する前にデフォルト値を注入しなければなりません。DefaultValuePipeがその役割を果たします。次に示すように、関連する@Parse*Pipeの前に@Query()デコレータでDeafultValuePipeをインスタンス化するだけです。
@Get()
async findAll(
@Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean,
@Query('page', new DefaultValuePipe(0), ParseIntPipe) page: number,
) {
return this.catsService.findAll({ activeOnly, page });
}

組み込みの検証Pipe

注意点として、ValidationPipeはNestJSによって提供されており、一般的な検証Pipeを独自に構築する必要はありません。組み込みのValidationPipeは本章で構築したサンプルよりも沢山のオプションを提供します。