Pipeとは
NestJSのPipeは、
@Injectable()
デコレータでアノテーションされたクラスで、PipeTransform
インターフェイスを実装しています。Pipeには2つの使い方があります。
- 変換:入力データを希望の型に変換する(例:
string
→number
) - 検証:入力データを評価し、データが正しければ実行を継続し、間違っていれば例外を投げる
いずれもPipeはControllerのルートハンドラによって処理される
arguments
を操作します。NestJSはメソッドが呼び出される直前にPipeを挿入し、Pipeはメソッドの引数を受け取って処理します。変換や検証はその時点で実施され、最後に変換された引数でルートハンドラが呼び出されます。
NestJSには組み込みの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クラスのインスタンスを適切なコンテキストにバインドする必要があります。
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は自分で構築できます。NestJSには堅牢な組み込みの
ParseIntPipe
とValidationPipe
がありますが、カスタム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;
}
これらのプロパティは、現在処理中の引数を記述します。
引数が
@Body()
、@Query()
、@Param()
やカスタムパラメータのいずれかであることを示します。引数のメタタイプ(例:
String
)を示します。ルートハンドラのメソッドシグネチャで型宣言を省略した場合、あるいはvanillaJSを使った場合、この値は未定義になります。@Body('string')
のように、デコレータに渡す文字列を指定します。デコレータの()
を空にしておくと未定義になります。注意
TypeScriptのインターフェイスはトランスパイル時に消滅します。したがって、メソッドパラメータの型がクラスではなくインターフェイスとして宣言されている場合、メタタイプの値は
Object
になります。検証Pipeをもう少し便利にしてみましょう。
CatsController
のcreate()
メソッドに注目します。ここで、来たポス トの本体であるオブジェクトが有効かどうか、サービスメソッドが起動する前に確かめられることを確認します。@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)な方法で実施するには、いくつかのアプローチがあります。一般的なアプローチの一つはスキーマベースの検証を行うことです。実際に試してみましょう。
まずは、以下のコマンドを入力してください。
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;
}
}