Interceptorとは
NestJSにおけるInterceptor(インターセプター)とは、
@Injectable()
デコレータでアノテーションされたクラスでNestInterceptor
インターフェイスを実装しています。Interceptorは、アスペクト指向プログラミング(AOP,
Aspect Oriented Programming
)の手法にヒントを得た、一連の便利な機能を備えております。これらの機能により、以下の様なことができるようになります。- メソッド実行の前後に余分なロジックをバインドする
- 関数から返される結果を変換する
- 関数からスローされる例外を変換する
- 関数の基本的な振る舞いを拡張する
- 特定の条件に応じて関数を完全にオーバーライドする
それぞれのInterceptorは
intercept()
メソッドを実装しており、2つの引数を取ります。最初の引数はExcecutionContext
のインスタンスです。ExcecutionContext
はArgumentsHost
を継承しています。ArgumentsHost
は例外フィルタの章で確認できます。ArgumentsHost
を継承したExcecutionContext
には、現在の実行処理に関するさらなる情報を提供するヘルパーメソッドもいく つか追加されています。これらのメソッドは、多種多様なコントローラやメソッド、実行コンテキストで動作する汎用的なInterceptorを作成する際には非常に有用です。第二引数には
CallHandler
を指定します。CallHandler
インターフェイスはhandler()
メソッドを実装しています。これを使うと、インターセプターのどこかでルートハンドラメソッドを呼び出すことができます。もしintercept()
メソッドの実装でhandle()
メソッドを呼ばなかった場合はルートハンドラメソッドは実行されません。この方式は、
intercept()
メソッドがリクエスト/レスポンスストリームを効果的にラップしていることを意味します。その結果、最終的なルートハンドラの実行前と実行後の両方でカスタムロジックを実装できます。intercept()
メソッドにhandle()
をコールする前に実行させるコードを書けるのは明白です。handle()
メソッドはObservable
を返すので、rxjs
ライブラリの強力な演算子を使ってレスポンスを操作できます。アスペクト指向プログラミングの用語を使うと、ルートハンドラの呼び出しはポイントカットと呼ばれ、追加ロジックが挿入されるポイントであることを示しています。最初に見ていくのは、インターセプターを使用してユーザとのやり取りを記録する場合です。以下にシンプルな
LoggingInterceptor
を示します。logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
const now = Date.now();
return next
.handle()
.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}
メモ
NestInterceptor<T, R>
とは、T
がObservable<T>
の型、R
がObservable<R>
でラップされた値の型を示す汎用インターフェイスです。注意
インターセプターは、コントローラやプロバイダ、ガードなどと同様にコンストラクタを通して依存性を注入できます。
handle()
はRxJSのObservable
を返すので、ストリームを操作するために使用できるオペレータの幅広い選択があります。上記の例では、tap()
演算子を使いました。これは、observable
ストリームの優雅な、または例外的な終了時に、匿名ログ関数を呼び出しますが、それ以外は応答サイクルに干渉しません。Interceptorを設定するためには、
@nestjs/common
パッケージからインポートした@UseInterceptors()
デコレータを使います。パイプやガードと同様に、インターセプターもコントローラスコープ、メソッドスコープやグローバルスコープのいずれかを指定できます。cats.controller.ts
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
メモ
@UseInterceptors()
デコレータは@nestjs/common
パッケージからインポートしています。上記の構成で、
CatsController
に定義された各ルートハンドラはLoggingInterceptor
を使うことになります。GET /cats
エンドポイントを呼び出すと、標準出力に次のような出力が表示されます。Before...
After... 1ms
インスタンスの代わりに
LoggingInterceptor
型を渡すことで、フレームワークにインスタンス化の責任を負わせ、依存性注入を可能にしていることに注意してください。パイプ、ガードや例外フィルタと同様にインプレイスインスタンスを渡すことができます。cats.controller.ts
@UseInterceptors(new LoggingInterceptor())
export class CatsController {}
先ほど説明したように、上記の構文ではこのコントローラで宣言されているすべてのハンドラにインターセプターをアタッチしています。もしインターセプターのスコープを一つのメソッドに限定 したい場合は、単純にメソッドレベルでデコレータを適用します。
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
グローバルインターセプターはアプリケーション全体、要はすべてのコントローラやルートハンドラで使用します。依存性の注入という観点では、モジュールの外から(上記では
useGlobalInterceptors()
で)登録したグローバルインターセプタは依存性を注入することができません。この問題を解決するために、以下のような構文で任意のモジュールから直接インターセプターを設定できます。
app.module.ts
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}
メモ
この方法でインターセプターの依存性注入を行う場合、この構造が採用されたモジュールに関係なくインターセプタは実際にはグローバルであることに留意してください。
依存性注入はインターセプターが定義されているモジュールの上(上記の例では
LoggingInterceptor
)で行いましょう。handle()
がObservable
を返すことは以前のセクションで説明しました。このストリームはルートハンドラから返された値を含んでいるので、RxJSのmap()
演算子を使って簡単に変異させられます。注意
レスポンスのマッピング機能は、ライブラリ固有のレスポンスストラテジーでは動作しません。(
@Res()
オブジェクトを直接使用することは禁止されています)TransformInterceptor
を作成して、些細な方法でそれぞれのレスポンスを変更します。RxJSのmap()
演算子を使って、レスポンスオブジェクトを新しく作成したオブジェクトのdata
プロパティに代入し、新しいオブジェクトをクライアントに返します。transform.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response<T> {
data: T;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next.handle().pipe(map(data => ({ data })));
}
}
メモ
NestJSのインターセプターは同期関数と非同期の
intercept()
メソッドの両方で機能します。必要に応じて、メソッドを非同期に切り替えるだけです。上記の構成で、誰かが
GET /cats
エンドポイントを呼び出すと、レスポンスは次のようになります。(ルートハンドラが空の配列[]
を返すことを仮定){
"data": []
}
インターセプターはアプリケーション全体で発生する要求に対して再利用可能なソリューションを作成する際に大きな価値を発揮します。
例えば、
null
が出現するたびにそれを空文字列''
に変換する必要が有ることを仮定しましょう。これを一行のコードで実現し、イ ンターセプターをグローバルにバインドしておけば、登録したハンドラで自動的に使用できるようになります。import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class ExcludeNullInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(map(value => value === null ? '' : value ));
}
}
もう一つの興味深い例は、RxJSの
catchError()
演算子を利用してあろーされた例外をオーバーライドすることです。errors.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
BadGatewayException,
CallHandler,
} from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
catchError(err => throwError(() => new BadGatewayException())),
);
}
}
ハンドラの呼び出しを完全に止めて、代わりに別の値を返したい理由はいくつかあります。わかりやすい例としては、キャッシュを実装してレスポンスタイムを向上させるというものがあります。
ここでは、キャッシュからレスポンスを返すシンプルなキャッシュインターセプターを見てみましょう。現実的な例では、TTLやキャッシュの無効化、キャッシュのサイズなど他の要素も考慮したいのですが、本セクションでは主な概念を示す基本的な例を示します。
cache.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
@Injectable()
export class CacheInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const isCached = true;
if (isCached) {
return of([]);
}
return next.handle();
}
}
CacheInterceptor
はハードコートされたisCached
変数とハードコードされたresponse[]
を同様に持っています。注意するべき点は、ここではRxJSのof()演算子によって作成された新しいストリームを返しているので、ルートハンドラが全く呼び出されないということです。CacheInterceptor
を利用するエンドポイントを呼び出すと、レスポンス(ハードコードされた空の配列)が直ちに返されます。汎用的なソリューションを作成するには、Reflector
を活用して独自のデコレータを作成します。RxJSの演算子を使ってストリームを操作することで、多くの機能を利用することができます。別の一般的なユースケースを考えてみましょう。
ルートリクエストのタイムアウトを処理することを想像してみてください。エンドポイントが一定時間経過して何も返さない場合はエラーレスポンスでプログラムを終了したいことを想定します。
以下のプログラムで上記の機能を実装できます。
timeout.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(5000),
catchError(err => {
if (err instanceof TimeoutError) {
return throwError(() => new RequestTimeoutException());
}
return throwError(() => err);
}),
);
};
};
5秒経過すると、リクエストはキャンセルされます。
RequestTimeoutException
を投げる前にカスタムロジックを追加することもできます。Last modified 8mo ago