NestJS公式ドキュメント翻訳
原文
Pipes| NestJS - A progressive Node.js web framework
パイプ
パイプは@Injectable()
デコレータが付けられたクラスです。パイプはPipeTransform
インターフェースを実装する必要があります。
パイプには2つの典型的な使用例があります。
- 変換:入力データを目的の出力に変換します
- バリデーション:入力データを評価し、有効であればそのまま変更せずに渡します。データが正しくないときは例外をスローします。
どちらの場合も、パイプはコントローラルートハンドラーによって処理されているarguments
で動作します。Nestはメソッドが呼び出される直前にパイプを挿入し、パイプはメソッド宛の引数を受け取ります。その時点で変換またはバリデートが行われた後、ルートハンドラーが(潜在的に)変換された引数で呼び出されます。
パイプは例外ゾーン内で実行されます。これはパイプが例外をスローすると例外レイヤー(グローバル例外フィルターと現在のコンテキストに適用される例外フィルター)によって処理されることを意味します。つまり、Pipeで例外がスローされるとその後コントローラメソッドは実行されません。
組み込みパイプ
NestにはValidationPipe
、ParseIntPipe
、ParseUUIDPipe
の3つのパイプがあり、すぐに使用することができます。@nestjs/common
パッケージからエクスポートされます。これらがどのように機能するかをよりよく理解するために、それらをゼロから構築してみましょう。
ValidationPipe
から始めましょう。まずは、単純に入力値を取得しすぐに同じ値を返すようにします。
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value;
}
}
PipeTransform<T, R>
は汎用インターフェイスで、T
は入力値の型を、R
はtransform()
メソッドの戻り値の型を示します。
すべてのパイプはtransform()
メソッドを提供する必要があります。このメソッドには2つのパラメーターがあります。
value
metadata
value
は現在(ルートハンドラーメソッドによって受信される前)処理されている引数であり、metadata
はそのメタデータです。メタデータオブジェクトには次のプロパティがあります。
export interface ArgumentMetadata {
type: 'body' | 'query' | 'param' | 'custom';
metatype?: Type<any>;
data?: string;
}
これらのプロパティは現在処理されている引数を記述します。
type |
引数がbody@Body() 、query@Query() 、param @Param() 、カスタムパラメーター(詳細はこちら)のどれかを示します。 |
metatype |
引数のメタタイプ(String など)を提供します。※ ルートハンドラーメソッドシグネチャで型宣言を省略するか、バニラJavaScriptを使用すると値はundefined になります。 |
data |
@Body('string') などデコレータに渡される文字列。デコレータの括弧を空のままにするとundefined になります。 |
TypeScriptインターフェースはトランスパイル時に消えます。したがって、メソッドパラメーターの型がクラスではなくインターフェースとして宣言されている場合、metatype
の値はObject
になります。
バリデーションのユースケース
CatsController
のcreate()
メソッドを詳しく見てみましょう。
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
createCatDto
bodyパラメーターに注目しましょう。型はCreateCatDtoです。
export class CreateCatDto {
readonly name: string;
readonly age: number;
readonly breed: string;
}
create
メソッドへのリクエストには有効なボディが含まれるようにします。そのため、createCatDto
オブジェクトの3つのメンバーをバリデートする必要があります。ルートハンドラーメソッド内でこれを実行することもできますが、単一責任原則(SRP)を破ります。別のアプローチとして、バリデータクラスを作成しそこにタスクを委任することもできますが、各メソッドの最初にこのバリデータを使用する必要があります。それではバリデートミドルウェアを作るのはどうでしょうか?これは良いアイデアかもしれませんが、アプリケーション全体で使用できる汎用ミドルウェアを作成することはできません(ミドルウェアは呼び出されるハンドラーとそのパラメーターを含む実行コンテキストを認識しないため)。
パイプは最適なケースです。それでは1つを作成しましょう。
オブジェクトスキーマバリデーション
オブジェクトのバリデーションにはいくつかのアプローチがあります。一般的なアプローチの1つは、スキーマベースのバリデーションを使用することです。Joiライブラリを使用すると、読み取り可能なAPIを使用して非常に簡単な方法でスキーマを作成できます。Joiベースのスキーマを利用するパイプを見てみましょう。
最初に必要なパッケージをインストールします。
$ npm install --save @hapi/joi
$ npm install --save-dev @types/hapi__joi
以下のサンプルコードでは、constructor
引数としてスキーマを使用する単純なクラスを作成します。次にschema.validate()
メソッドを適用します。このメソッドは指定されたスキーマに対して引数を検証します。
前述のように、バリデーションパイプは値を変更せずに返すか例外をスローします。
次のセクションでは、@UsePipes()
デコレータを使用して特定のコントローラメソッドに適切なスキーマを提供する方法を説明します。
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class JoiValidationPipe implements PipeTransform {
constructor(private readonly schema: Object) {}
transform(value: any, metadata: ArgumentMetadata) {
const { error } = this.schema.validate(value);
if (error) {
throw new BadRequestException('Validation failed');
}
return value;
}
}
パイプのバインディング
パイプのバインディング(適切なコントローラまたはハンドラーへのパイプ処理)は非常に簡単です。@UsePipes()
デコレータを使用してパイプインスタンスを作成し、Joiバリデーションスキーマを渡します。
@Post()
@UsePipes(new JoiValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
Class validator
このセクションの手法にはTypeScriptが必要であり、アプリがバニラJavaScriptを使用して記述されている場合は利用できません。
バリデーション方法の代替実装を見てみましょう。
Nestはclass-validatorライブラリと連携して動作します。このすばらしいライブラリを使うと、デコレータベースのバリデーションを行うことができます。処理されたプロパティのメタタイプにアクセスできるため、デコレータベースのバリデーションは特にNestのパイプ機能と組み合わせると非常に強力です。使用前に必要なパッケージをインストールする必要があります。
$ npm i --save class-validator class-transformer
インストールできたらCreateCatDto
クラスにいくつかのデコレータを追加します。
import { IsString, IsInt } from 'class-validator';
export class CreateCatDto {
@IsString()
readonly name: string;
@IsInt()
readonly age: number;
@IsString()
readonly breed: string;
}
class-validatorデコレータの詳細については、こちらをご覧ください。
これでValidationPipe
クラスを作成できます。
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);
}
}
上記ではclass-transformerライブラリを使用しました。class-validatorライブラリと同じ作成者が作成しており非常にうまく機能します。
このコードを見ていきましょう。まず、transform()
関数がasync
であることに注意してください。これは、Nestは同期パイプと非同期パイプの両方をサポートしており、またclass-validatorのバリデートの一部が非同期(Promiseを利用)にできるためです。
次に、構造化することでmetatype
パラメーターとしてメタタイプフィールドを取り出します(ArgumentMetadata
からこのメンバーだけを抽出します)。これは完全なArgumentMetadata
を取得し、メタタイプ変数を割り当てるための追加ステートメントを使用するための略記です。
次に、ヘルパー関数toValidate()
を見てください。処理中の現在の引数がネイティブJavaScript型である場合、バリデーションステップをパスする責任があります(これらはスキーマをアタッチできないためバリデーションステップを実行する理由はありません)。
次に、class-transformer関数plainToClass()
を使用して、プレーンJavaScript引数オブジェクトを型付きオブジェクトに変換しバリデーションを適用できるようにします。ネットワークリクエストからデシリアライズされる際、ボディは型情報を持ちません。class-validatorは、以前にDTOに対して定義したバリデーションデコレータを使用する必要があるため、この変換を実行する必要があります。
そして前述のとおり、これはバリデーションパイプであるため値を変更せずに返すか例外をスローします。
最後のステップとしてValidationPipe
をバインドします。例外フィルターと同様に、パイプはメソッドスコープ、コントローラスコープ、またはグローバルスコープにすることができます。さらに、パイプはパラメータースコープにすることもできます。以下の例では、パイプインスタンスをルートパラメーター@Body()
デコレータに直接結び付けます。
@Post()
async create(
@Body(new ValidationPipe()) createCatDto: CreateCatDto,
) {
this.catsService.create(createCatDto);
}
パラメータスコープのパイプは、バリデーションロジックが1つの特定のパラメーターのみに関係する場合に役立ちます。
または、メソッドレベルでパイプを設定するには@UsePipes()
デコレータを使用します。
@Post()
@UsePipes(new ValidationPipe())
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@UsePipes()
デコレータは@nestjs/common
パッケージからインポートされます。
上記の例では、ValidationPipe
のインスタンスがすぐに作成されています。または、(インスタンスではなく)クラスを渡すことでインスタンス化をフレームワークに任せ、依存性注入を有効にします。
@Post()
@UsePipes(ValidationPipe)
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
ValidationPipe
は非常に汎用的に作成されているため、アプリケーション全体のすべてのルートハンドラーに適用されるグローバルスコープのパイプとして設定してみましょう。
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
ハイブリッドアプリの場合、useGlobalPipes()
メソッドはゲートウェイおよびマイクロサービス用のパイプを設定しません。 「スタンダード(非ハイブリッド)」マイクロサービスアプリの場合、useGlobalPipes()
はパイプをグローバルにマウントします。
グローバルパイプは、すべてのコントローラとルートハンドラーに対してアプリケーション全体で使用されます。依存性注入に関しては、モジュールの外部で登録されたグローバルパイプ(上記の例のようなuseGlobalPipes()
)は、モジュールのコンテキスト外で行われるため依存性を注入できません。次のようにすることで、任意のモジュールからグローバルパイプを直接設定しこの問題を解決することができます。
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}
このアプローチでパイプの依存性注入を実行する場合、この構造が使用されるモジュールに関係なく、パイプは実際にはグローバルであることに注意してください。これはどこで行われるのでしょうか?パイプ(上記の例ではValidationPipe
)が定義されているモジュールを選択します。また、カスタムプロバイダの登録を処理する方法はuseClass
だけではありません。詳細はこちらをご覧ください。
変換のユースケース
パイプのユースケースはバリデーションだけではありません。この章の冒頭で、パイプを使用して入力データを目的の出力に変換できることを説明しました。これはtransform
関数から返された値が引数の以前の値を完全にオーバーライドするため実現することができます。どのような場面で役に立つでしょうか?クライアントから渡されたデータはルートハンドラーメソッドによって適切に処理される前に、文字列を整数に変換するなど何らかの変更が必要になることがあります。さらに、一部の必須データフィールドが欠落している可能性があるため、デフォルト値を適用したい場合もあるでしょう。変換パイプはクライアントリクエストとリクエストハンドラの間に処理機能を挿入することにより、これらの機能を実行できます。
これは文字列を整数値に解析するためのParseIntPipe
です。
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;
}
}
以下のように、このパイプを選択したパラメーターに簡単に結びつけることができます。
@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
return await this.catsService.findOne(id);
}
必要に応じて、文字列のパースを担当するParseUUIDPipe
を使用し、UUIDかどうかを検証することができます。
@Get(':id')
async findOne(@Param('id', new ParseUUIDPipe()) id) {
return await this.catsService.findOne(id);
}
ParseUUIDPipe()
を使用する場合、バージョン3,4,5でUUIDをパースしています。特定のバージョンのUUIDのみが必要な場合は、パイプオプションでバージョンを渡すことができます。
これを設定すると、リクエストが対応するハンドラーに到達する前にParseIntPipe
またはParseUUIDPipe
が実行され、id
パラメーターの整数またはUUIDを常に受け取るようになります。
別の便利なケースは、IDによってデータベースから既存のユーザーエンティティを選択するケースです。
@Get(':id')
findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
return userEntity;
}
このパイプの実装は読者に任せます。他のすべての変換パイプと同様に、入力値(id
)を受け取り、出力値(UserEntity
オブジェクト)を返すことに注意してください。ハンドラーから共通パイプへボイラープレートコードを抽象化することにより、コードをより宣言的でDRYにすることができます。
組み込みのValidationPipe
幸いなことに、ValidationPipe
とParseIntPipe
はNestで提供されているため、これらのパイプを自分で作成する必要はありません。 (ValidationPipe
ではclass-validator
とclass-transformer
パッケージの両方をインストールする必要があることに注意してください)。
組み込みのValidationPipe
は、この章で作成したサンプルよりも多くのオプションを提供しています。サンプルではパイプの基本的な仕組みを説明しました。ここでさらに多くの例を見ることができます。
そのようなオプションの1つがtransform
です。シリアル化されていないbodyオブジェクトがバニラJavaScriptオブジェクトである(DTOタイプを持たない)という以前の議論を思い出してください。これまで、パイプを使用してペイロードを検証してきました。バリデーションができるように、その過程でclass-transform
を使用してプレーンオブジェクトを型付きオブジェクトに一時的に変換したことを思い出してください。組み込みのValidationPipe
はオプションでこの変換されたオブジェクトを返すこともできます。構成オブジェクトをパイプに渡すことにより、この動作を有効にします。このオプションの場合、以下に示すように、値がtrue
のフィールドtransform
を持つ構成オブジェクトを渡します。
@Post()
@UsePipes(new ValidationPipe({ transform: true }))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
ValidationPipe
は@nestjs/common
パッケージからインポートされます。
このパイプはclass-validator
ライブラリとclass-transformer
ライブラリに基づいているため、多くの追加オプションが利用可能です。上記のtransform
オプションと同様に、パイプに渡された構成オブジェクトを介してこれらの設定を構成します。組み込みオプションは次のとおりです。
export interface ValidationPipeOptions extends ValidatorOptions {
transform?: boolean;
disableErrorMessages?: boolean;
exceptionFactory?: (errors: ValidationError[]) => any;
}
これらに加えて、すべてのclass-validator
オプション(ValidatorOptions
インターフェイスから継承)が利用可能です:
オプション | 型 | 説明 |
---|---|---|
skipMissingProperties |
boolean |
trueに設定すると、バリデータはバリデーションオブジェクトにないすべてのプロパティのバリデーションをスキップします。 |
whitelist |
boolean |
trueに設定すると、バリデータはバリデーションデコレータを使用しないプロパティの検証済み(返された)オブジェクトを取り除きます。 |
forbidNonWhitelisted |
boolean |
trueに設定すると、ホワイトリストに登録されていないプロパティバリデータを削除する代わりに例外がスローされます。 |
forbidUnknownValues |
boolean |
trueに設定すると、不明なオブジェクトをバリデートしようとするとすぐに失敗します。 |
disableErrorMessages |
boolean |
trueに設定すると、バリデーションエラーはクライアントに返されません。 |
exceptionFactory |
Function |
バリデーションエラーの配列を受け取り、スローされる例外オブジェクトを返します。 |
groups |
string[] |
オブジェクトのバリデーション中に使用されるグループです。 |
dismissDefaultMessages |
boolean |
trueに設定されている場合、バリデーションではデフォルトのメッセージは使用されません。明示的に設定されていない場合エラーメッセージは常にundefined になります。 |
validationError.target |
boolean |
ValidationError で対象を公開するかどうかを示します。 |
validationError.value |
boolean |
ValidationError でバリデーション済みの値を公開するかどうかを示します。 |
class-validator
パッケージの詳細についてはリポジトリをご覧ください。