Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

Pipes | NestJS 【翻訳】

More than 1 year has passed since last update.

NestJS公式ドキュメント翻訳

NestJS公式ドキュメント翻訳

原文

Pipes| NestJS - A progressive Node.js web framework

パイプ

パイプは@Injectable()デコレータが付けられたクラスです。パイプはPipeTransformインターフェースを実装する必要があります。

キャプチャ.PNG

パイプには2つの典型的な使用例があります。

  • 変換:入力データを目的の出力に変換します
  • バリデーション:入力データを評価し、有効であればそのまま変更せずに渡します。データが正しくないときは例外をスローします。

どちらの場合も、パイプはコントローラルートハンドラーによって処理されているargumentsで動作します。Nestはメソッドが呼び出される直前にパイプを挿入し、パイプはメソッド宛の引数を受け取ります。その時点で変換またはバリデートが行われた後、ルートハンドラーが(潜在的に)変換された引数で呼び出されます。

パイプは例外ゾーン内で実行されます。これはパイプが例外をスローすると例外レイヤー(グローバル例外フィルターと現在のコンテキストに適用される例外フィルター)によって処理されることを意味します。つまり、Pipeで例外がスローされるとその後コントローラメソッドは実行されません。

組み込みパイプ

NestにはValidationPipeParseIntPipeParseUUIDPipeの3つのパイプがあり、すぐに使用することができます。@nestjs/commonパッケージからエクスポートされます。これらがどのように機能するかをよりよく理解するために、それらをゼロから構築してみましょう。

ValidationPipeから始めましょう。まずは、単純に入力値を取得しすぐに同じ値を返すようにします。

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>は汎用インターフェイスで、Tは入力値の型を、Rtransform()メソッドの戻り値の型を示します。

すべてのパイプは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になります。

バリデーションのユースケース

CatsControllercreate()メソッドを詳しく見てみましょう。

@Post()
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

createCatDtobodyパラメーターに注目しましょう。型はCreateCatDtoです。

create-cat.dto.ts
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クラスにいくつかのデコレータを追加します。

create-cat.dto.ts
import { IsString, IsInt } from 'class-validator';

export class CreateCatDto {
  @IsString()
  readonly name: string;

  @IsInt()
  readonly age: number;

  @IsString()
  readonly breed: string;
}

class-validatorデコレータの詳細については、こちらをご覧ください。

これで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);
  }
}

上記ではclass-transformerライブラリを使用しました。class-validatorライブラリと同じ作成者が作成しており非常にうまく機能します。

このコードを見ていきましょう。まず、transform()関数がasyncであることに注意してください。これは、Nestは同期パイプと非同期パイプの両方をサポートしており、またclass-validatorのバリデートの一部が非同期(Promiseを利用)にできるためです。

次に、構造化することでmetatypeパラメーターとしてメタタイプフィールドを取り出します(ArgumentMetadataからこのメンバーだけを抽出します)。これは完全なArgumentMetadataを取得し、メタタイプ変数を割り当てるための追加ステートメントを使用するための略記です。

次に、ヘルパー関数toValidate()を見てください。処理中の現在の引数がネイティブJavaScript型である場合、バリデーションステップをパスする責任があります(これらはスキーマをアタッチできないためバリデーションステップを実行する理由はありません)。

次に、class-transformer関数plainToClass()を使用して、プレーンJavaScript引数オブジェクトを型付きオブジェクトに変換しバリデーションを適用できるようにします。ネットワークリクエストからデシリアライズされる際、ボディは型情報を持ちません。class-validatorは、以前にDTOに対して定義したバリデーションデコレータを使用する必要があるため、この変換を実行する必要があります。

そして前述のとおり、これはバリデーションパイプであるため値を変更せずに返すか例外をスローします。

最後のステップとしてValidationPipeをバインドします。例外フィルターと同様に、パイプはメソッドスコープ、コントローラスコープ、またはグローバルスコープにすることができます。さらに、パイプはパラメータースコープにすることもできます。以下の例では、パイプインスタンスをルートパラメーター@Body()デコレータに直接結び付けます。

cats.controller.ts
@Post()
async create(
  @Body(new ValidationPipe()) createCatDto: CreateCatDto,
) {
  this.catsService.create(createCatDto);
}

パラメータスコープのパイプは、バリデーションロジックが1つの特定のパラメーターのみに関係する場合に役立ちます。

または、メソッドレベルでパイプを設定するには@UsePipes()デコレータを使用します。

cats.controller.ts
@Post()
@UsePipes(new ValidationPipe())
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

@UsePipes()デコレータは@nestjs/commonパッケージからインポートされます。

上記の例では、ValidationPipeのインスタンスがすぐに作成されています。または、(インスタンスではなく)クラスを渡すことでインスタンス化をフレームワークに任せ、依存性注入を有効にします。

cats.controller.ts
@Post()
@UsePipes(ValidationPipe)
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

ValidationPipeは非常に汎用的に作成されているため、アプリケーション全体のすべてのルートハンドラーに適用されるグローバルスコープのパイプとして設定してみましょう。

main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

ハイブリッドアプリの場合、useGlobalPipes()メソッドはゲートウェイおよびマイクロサービス用のパイプを設定しません。 「スタンダード(非ハイブリッド)」マイクロサービスアプリの場合、useGlobalPipes()はパイプをグローバルにマウントします。

グローバルパイプは、すべてのコントローラとルートハンドラーに対してアプリケーション全体で使用されます。依存性注入に関しては、モジュールの外部で登録されたグローバルパイプ(上記の例のようなuseGlobalPipes())は、モジュールのコンテキスト外で行われるため依存性を注入できません。次のようにすることで、任意のモジュールからグローバルパイプを直接設定しこの問題を解決することができます。

app.module.ts
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です。

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;
  }
}

以下のように、このパイプを選択したパラメーターに簡単に結びつけることができます。

@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

幸いなことに、ValidationPipeParseIntPipeはNestで提供されているため、これらのパイプを自分で作成する必要はありません。 (ValidationPipeではclass-validatorclass-transformerパッケージの両方をインストールする必要があることに注意してください)。

組み込みのValidationPipeは、この章で作成したサンプルよりも多くのオプションを提供しています。サンプルではパイプの基本的な仕組みを説明しました。ここでさらに多くの例を見ることができます。

そのようなオプションの1つがtransformです。シリアル化されていないbodyオブジェクトがバニラJavaScriptオブジェクトである(DTOタイプを持たない)という以前の議論を思い出してください。これまで、パイプを使用してペイロードを検証してきました。バリデーションができるように、その過程でclass-transformを使用してプレーンオブジェクトを型付きオブジェクトに一時的に変換したことを思い出してください。組み込みのValidationPipeはオプションでこの変換されたオブジェクトを返すこともできます。構成オブジェクトをパイプに渡すことにより、この動作を有効にします。このオプションの場合、以下に示すように、値がtrueのフィールドtransformを持つ構成オブジェクトを渡します。

cats.controller.ts
@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パッケージの詳細についてはリポジトリをご覧ください。

mana-vv
Software Engineer
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away