NestJSのInterceptorは、リクエストとレスポンスの前後に任意のロジックを挿入できる強力な機能です。AOP(Aspect Oriented Programming)の考え方に基づき、ロギング、キャッシュ、レスポンス変換、タイムアウト処理などの横断的関心事を、ビジネスロジックから分離して実装できます。本記事では、NestInterceptorインターフェースとCallHandlerの基本から、RxJSオペレータを活用した高度なレスポンス加工パターンまでを詳しく解説します。
実行環境と前提条件#
本記事の内容を実践するにあたり、以下の環境を前提としています。
| 項目 |
バージョン・要件 |
| Node.js |
20以上 |
| npm |
10以上 |
| NestJS |
11.x |
| RxJS |
7.x(NestJSに同梱) |
| OS |
Windows / macOS / Linux |
| エディタ |
VS Code(推奨) |
事前に以下の準備を完了してください。
- NestJS CLIのインストール済み
- NestJSプロジェクトの作成済み
- RxJSの基本概念(Observable、pipe、オペレータ)の理解
プロジェクトの作成方法はNestJS入門記事を参照してください。Module・Controller・Providerの基本はアーキテクチャ解説記事で確認できます。
Interceptorとは#
Interceptorは、@Injectable()デコレータを持ち、NestInterceptorインターフェースを実装するクラスです。AOPの概念にインスパイアされており、以下の機能を実現できます。
- メソッド実行の前後に追加ロジックをバインド
- 関数から返される結果の変換
- スローされた例外の変換
- 基本的な関数の振る舞いの拡張
- 特定の条件に応じた関数の完全なオーバーライド(キャッシュ等)
NestJSリクエストライフサイクルにおけるInterceptorの位置#
Interceptorは、GuardとPipeの間で実行されますが、レスポンス処理時にも再度実行される点が特徴的です。
flowchart LR
A[Client Request] --> B[Middleware]
B --> C[Guard]
C --> D[Interceptor<br/>前処理]
D --> E[Pipe]
E --> F[Controller]
F --> G[Interceptor<br/>後処理]
G --> H[Response]
style D fill:#10b981,color:#fff
style G fill:#10b981,color:#fff
| 実行順序 |
コンポーネント |
主な役割 |
| 1 |
Middleware |
リクエスト前処理、ロギング |
| 2 |
Guard |
認可チェック |
| 3 |
Interceptor(前処理) |
リクエスト変換、実行時間計測開始 |
| 4 |
Pipe |
バリデーション、データ変換 |
| 5 |
Controller |
ビジネスロジック実行 |
| 6 |
Interceptor(後処理) |
レスポンス変換、ロギング完了 |
MiddlewareとInterceptorの違い#
MiddlewareとInterceptorはどちらもリクエスト処理に介入しますが、重要な違いがあります。
| 観点 |
Middleware |
Interceptor |
| 実行タイミング |
リクエスト到達前のみ |
リクエスト前後の両方 |
| レスポンスへのアクセス |
制限あり |
RxJSで完全な制御可能 |
| ExecutionContext |
なし |
あり(実行コンテキスト情報) |
| DI対応 |
限定的 |
完全対応 |
| 主な用途 |
認証、CORS、ロギング |
レスポンス変換、キャッシュ、計測 |
NestInterceptorインターフェースの基本#
Interceptorを作成するには、NestInterceptorインターフェースのintercept()メソッドを実装します。
intercept()メソッドの構造#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class ExampleInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// 前処理のロジック
console.log('Before handler execution...');
// next.handle()でルートハンドラを実行し、Observableを返す
return next.handle();
}
}
|
ExecutionContextの役割#
ExecutionContextは、現在の実行プロセスに関する詳細情報を提供します。ArgumentsHostを継承しており、以下のヘルパーメソッドを追加で提供します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@Injectable()
export class ContextAwareInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// 現在のクラス(コントローラ)を取得
const controller = context.getClass();
console.log(`Controller: ${controller.name}`);
// 現在のハンドラ(メソッド)を取得
const handler = context.getHandler();
console.log(`Handler: ${handler.name}`);
// HTTPコンテキストを取得
const ctx = context.switchToHttp();
const request = ctx.getRequest();
const response = ctx.getResponse();
console.log(`Method: ${request.method}, URL: ${request.url}`);
return next.handle();
}
}
|
CallHandlerの重要性#
CallHandlerインターフェースはhandle()メソッドを実装しており、これを呼び出すことでルートハンドラメソッドが実行されます。handle()を呼び出さなければ、ルートハンドラは一切実行されません。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Injectable()
export class ConditionalInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
// 特定の条件でハンドラの実行をスキップ
if (request.headers['x-skip-handler']) {
// handle()を呼ばずに直接レスポンスを返す
return of({ message: 'Handler was skipped' });
}
// 通常はhandle()を呼び出してハンドラを実行
return next.handle();
}
}
|
handle()はObservableを返すため、RxJSオペレータを使用してレスポンスストリームを操作できます。これがAOPにおける「Pointcut(ポイントカット)」であり、追加ロジックを挿入する地点となります。
RxJSオペレータによるレスポンス操作#
RxJSの豊富なオペレータを使用することで、レスポンスを柔軟に操作できます。
tapオペレータによるロギング#
tapオペレータは、ストリームの値を変更せずに副作用を実行します。ロギングに最適です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
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> {
const request = context.switchToHttp().getRequest();
const method = request.method;
const url = request.url;
const now = Date.now();
console.log(`[Request] ${method} ${url}`);
return next.handle().pipe(
tap({
next: (data) => {
const responseTime = Date.now() - now;
console.log(`[Response] ${method} ${url} - ${responseTime}ms`);
},
error: (error) => {
const responseTime = Date.now() - now;
console.log(`[Error] ${method} ${url} - ${responseTime}ms - ${error.message}`);
},
}),
);
}
}
|
実行結果の例#
1
2
|
[Request] GET /users/1
[Response] GET /users/1 - 23ms
|
mapオペレータによるレスポンス変換#
mapオペレータは、ストリームの値を変換します。統一的なレスポンス形式の実装に活用できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface ApiResponse<T> {
success: boolean;
data: T;
timestamp: string;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponse<T>> {
return next.handle().pipe(
map((data) => ({
success: true,
data,
timestamp: new Date().toISOString(),
})),
);
}
}
|
変換前後のレスポンス比較#
変換前(Controllerの戻り値):
1
2
3
4
5
|
{
"id": 1,
"name": "John Doe",
"email": "john@example.com"
}
|
変換後(Interceptor適用後):
1
2
3
4
5
6
7
8
9
|
{
"success": true,
"data": {
"id": 1,
"name": "John Doe",
"email": "john@example.com"
},
"timestamp": "2026-01-06T08:00:00.000Z"
}
|
catchErrorオペレータによる例外変換#
catchErrorオペレータは、エラーをキャッチして別のObservableに変換したり、エラーを再スローしたりできます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
HttpException,
HttpStatus,
} 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((error) => {
// ログ出力
console.error('Error caught in interceptor:', error.message);
// 特定のエラーを変換
if (error.code === 'ECONNREFUSED') {
return throwError(
() => new HttpException('Service temporarily unavailable', HttpStatus.SERVICE_UNAVAILABLE),
);
}
// その他のエラーはそのまま再スロー
return throwError(() => error);
}),
);
}
}
|
timeoutオペレータによるタイムアウト処理#
長時間実行されるリクエストに対してタイムアウトを設定できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
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 {
constructor(private readonly timeoutMs: number = 5000) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(this.timeoutMs),
catchError((error) => {
if (error instanceof TimeoutError) {
return throwError(() => new RequestTimeoutException('Request timeout'));
}
return throwError(() => error);
}),
);
}
}
|
finalizeオペレータによるクリーンアップ#
finalizeオペレータは、Observableが完了またはエラーで終了した際に実行されます。リソースの解放やメトリクス送信に利用できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
@Injectable()
export class MetricsInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const startTime = Date.now();
const handler = context.getHandler().name;
const controller = context.getClass().name;
return next.handle().pipe(
finalize(() => {
const duration = Date.now() - startTime;
// メトリクスサービスに送信(例: Prometheus, DataDog等)
console.log(`[Metrics] ${controller}.${handler} completed in ${duration}ms`);
}),
);
}
}
|
実践的なInterceptor実装パターン#
実行時間計測Interceptor#
API のパフォーマンスを計測するInterceptorを実装します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class PerformanceInterceptor implements NestInterceptor {
private readonly logger = new Logger(PerformanceInterceptor.name);
private readonly slowThresholdMs = 1000; // 1秒以上で警告
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url } = request;
const controller = context.getClass().name;
const handler = context.getHandler().name;
const startTime = Date.now();
return next.handle().pipe(
tap({
next: () => {
const duration = Date.now() - startTime;
const logMessage = `${method} ${url} [${controller}.${handler}] - ${duration}ms`;
if (duration > this.slowThresholdMs) {
this.logger.warn(`Slow request: ${logMessage}`);
} else {
this.logger.log(logMessage);
}
},
error: (error) => {
const duration = Date.now() - startTime;
this.logger.error(
`${method} ${url} [${controller}.${handler}] - ${duration}ms - Error: ${error.message}`,
);
},
}),
);
}
}
|
キャッシュInterceptor#
単純なインメモリキャッシュを実装するInterceptorです。実運用ではRedis等の外部キャッシュを使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class CacheInterceptor implements NestInterceptor {
private cache = new Map<string, { data: any; expiry: number }>();
private readonly ttlMs = 60000; // 1分
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
// GETリクエストのみキャッシュ対象
if (request.method !== 'GET') {
return next.handle();
}
const cacheKey = this.generateCacheKey(request);
const cached = this.cache.get(cacheKey);
// キャッシュヒットかつ有効期限内
if (cached && cached.expiry > Date.now()) {
console.log(`Cache hit: ${cacheKey}`);
return of(cached.data);
}
// キャッシュミス:ハンドラを実行してキャッシュに保存
return next.handle().pipe(
tap((data) => {
this.cache.set(cacheKey, {
data,
expiry: Date.now() + this.ttlMs,
});
console.log(`Cache set: ${cacheKey}`);
}),
);
}
private generateCacheKey(request: any): string {
const { url, query } = request;
return `${url}:${JSON.stringify(query)}`;
}
}
|
レスポンス除外Interceptor#
特定のフィールドをレスポンスから除外するInterceptorです。パスワードなどの機密情報を隠すのに使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class ExcludeFieldsInterceptor implements NestInterceptor {
constructor(private readonly fieldsToExclude: string[] = ['password', 'secret']) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => this.excludeFields(data)),
);
}
private excludeFields(data: any): any {
if (Array.isArray(data)) {
return data.map((item) => this.excludeFields(item));
}
if (data !== null && typeof data === 'object') {
const result = { ...data };
for (const field of this.fieldsToExclude) {
delete result[field];
}
// ネストされたオブジェクトも処理
for (const key of Object.keys(result)) {
if (typeof result[key] === 'object') {
result[key] = this.excludeFields(result[key]);
}
}
return result;
}
return data;
}
}
|
カスタムデコレータとReflectorの組み合わせ#
メタデータを使用して、特定のハンドラに対してInterceptorの動作を変更できます。
1
2
3
4
5
|
// decorators/skip-interceptor.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const SKIP_TRANSFORM_KEY = 'skipTransform';
export const SkipTransform = () => SetMetadata(SKIP_TRANSFORM_KEY, true);
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
// interceptors/conditional-transform.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { SKIP_TRANSFORM_KEY } from '../decorators/skip-interceptor.decorator';
@Injectable()
export class ConditionalTransformInterceptor implements NestInterceptor {
constructor(private readonly reflector: Reflector) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const skipTransform = this.reflector.get<boolean>(
SKIP_TRANSFORM_KEY,
context.getHandler(),
);
if (skipTransform) {
// メタデータがあればそのまま返す
return next.handle();
}
// メタデータがなければ変換を適用
return next.handle().pipe(
map((data) => ({
success: true,
data,
timestamp: new Date().toISOString(),
})),
);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// users.controller.ts
import { Controller, Get } from '@nestjs/common';
import { SkipTransform } from './decorators/skip-interceptor.decorator';
@Controller('users')
export class UsersController {
@Get()
findAll() {
// このエンドポイントは変換が適用される
return [{ id: 1, name: 'John' }];
}
@Get('raw')
@SkipTransform()
findAllRaw() {
// このエンドポイントは変換がスキップされる
return [{ id: 1, name: 'John' }];
}
}
|
Interceptorの適用方法#
Interceptorは、メソッドレベル、コントローラレベル、グローバルレベルで適用できます。
メソッドレベルでの適用#
特定のルートハンドラにのみInterceptorを適用します。
1
2
3
4
5
6
7
8
9
10
11
|
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from './interceptors/logging.interceptor';
@Controller('users')
export class UsersController {
@Get(':id')
@UseInterceptors(LoggingInterceptor)
findOne(@Param('id') id: string) {
return { id, name: 'John Doe' };
}
}
|
コントローラレベルでの適用#
コントローラ内のすべてのルートハンドラにInterceptorを適用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from './interceptors/logging.interceptor';
@UseInterceptors(LoggingInterceptor)
@Controller('users')
export class UsersController {
@Get()
findAll() {
return [];
}
@Get(':id')
findOne(@Param('id') id: string) {
return { id, name: 'John Doe' };
}
}
|
グローバルレベルでの適用#
アプリケーション全体にInterceptorを適用する方法は2つあります。
main.tsでの登録(DIなし)#
1
2
3
4
5
6
7
8
9
10
11
|
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LoggingInterceptor } from './interceptors/logging.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
await app.listen(3000);
}
bootstrap();
|
モジュールでの登録(DI対応)#
依存性注入を使用する場合は、APP_INTERCEPTORトークンを使用してモジュールに登録します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// app.module.ts
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { LoggingInterceptor } from './interceptors/logging.interceptor';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}
|
この方法を使用すると、Interceptor内で他のサービスをコンストラクタインジェクションできます。
1
2
3
4
5
6
7
8
9
10
|
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
constructor(private readonly configService: ConfigService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const logLevel = this.configService.get('LOG_LEVEL');
// ConfigServiceを使用したロジック
return next.handle();
}
}
|
複数Interceptorの適用順序#
複数のInterceptorを適用する場合、左から右の順序で前処理が実行され、右から左の順序で後処理が実行されます。
1
2
3
|
@UseInterceptors(InterceptorA, InterceptorB, InterceptorC)
@Controller('users')
export class UsersController {}
|
flowchart LR
subgraph 前処理
A1[InterceptorA] --> B1[InterceptorB] --> C1[InterceptorC]
end
C1 --> H[Handler]
subgraph 後処理
H --> C2[InterceptorC] --> B2[InterceptorB] --> A2[InterceptorA]
endRxJSオペレータの組み合わせパターン#
複数のオペレータを組み合わせて、より複雑な処理を実現できます。
ロギング + 変換 + タイムアウトの複合パターン#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
RequestTimeoutException,
Logger,
} from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { tap, map, timeout, catchError, finalize } from 'rxjs/operators';
export interface StandardResponse<T> {
success: boolean;
data: T;
meta: {
timestamp: string;
duration: number;
};
}
@Injectable()
export class ComprehensiveInterceptor<T> implements NestInterceptor<T, StandardResponse<T>> {
private readonly logger = new Logger(ComprehensiveInterceptor.name);
private readonly timeoutMs = 10000;
intercept(context: ExecutionContext, next: CallHandler): Observable<StandardResponse<T>> {
const request = context.switchToHttp().getRequest();
const { method, url } = request;
const startTime = Date.now();
this.logger.log(`Incoming request: ${method} ${url}`);
return next.handle().pipe(
// タイムアウト設定
timeout(this.timeoutMs),
// 成功時のログ出力
tap((data) => {
this.logger.log(`Response ready for: ${method} ${url}`);
}),
// レスポンス形式の変換
map((data) => ({
success: true,
data,
meta: {
timestamp: new Date().toISOString(),
duration: Date.now() - startTime,
},
})),
// エラーハンドリング
catchError((error) => {
if (error instanceof TimeoutError) {
this.logger.error(`Request timeout: ${method} ${url}`);
return throwError(() => new RequestTimeoutException());
}
this.logger.error(`Request failed: ${method} ${url} - ${error.message}`);
return throwError(() => error);
}),
// 完了時のクリーンアップ
finalize(() => {
const duration = Date.now() - startTime;
this.logger.log(`Request completed: ${method} ${url} - ${duration}ms`);
}),
);
}
}
|
Interceptorのテスト#
Interceptorは単体テストで動作を検証できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
import { Test, TestingModule } from '@nestjs/testing';
import { ExecutionContext, CallHandler } from '@nestjs/common';
import { of } from 'rxjs';
import { TransformInterceptor } from './transform.interceptor';
describe('TransformInterceptor', () => {
let interceptor: TransformInterceptor<any>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [TransformInterceptor],
}).compile();
interceptor = module.get<TransformInterceptor<any>>(TransformInterceptor);
});
it('should wrap response in standard format', (done) => {
const mockExecutionContext = {
switchToHttp: () => ({
getRequest: () => ({}),
getResponse: () => ({}),
}),
getHandler: () => ({}),
getClass: () => ({}),
} as unknown as ExecutionContext;
const mockCallHandler: CallHandler = {
handle: () => of({ id: 1, name: 'Test' }),
};
interceptor.intercept(mockExecutionContext, mockCallHandler).subscribe({
next: (result) => {
expect(result.success).toBe(true);
expect(result.data).toEqual({ id: 1, name: 'Test' });
expect(result.timestamp).toBeDefined();
done();
},
});
});
it('should handle empty response', (done) => {
const mockExecutionContext = {
switchToHttp: () => ({
getRequest: () => ({}),
getResponse: () => ({}),
}),
getHandler: () => ({}),
getClass: () => ({}),
} as unknown as ExecutionContext;
const mockCallHandler: CallHandler = {
handle: () => of(null),
};
interceptor.intercept(mockExecutionContext, mockCallHandler).subscribe({
next: (result) => {
expect(result.success).toBe(true);
expect(result.data).toBeNull();
done();
},
});
});
});
|
まとめ#
NestJSのInterceptorは、AOPパターンを活用してリクエスト・レスポンスの前後処理を実装する強力な機能です。本記事で解説したポイントをまとめます。
| 概念 |
説明 |
| NestInterceptor |
Interceptorの基本インターフェース |
| ExecutionContext |
実行コンテキスト情報(Controller、Handler等) |
| CallHandler |
ルートハンドラを呼び出すためのインターフェース |
| RxJS pipe |
複数のオペレータをチェーンして処理を組み合わせる |
| tap |
副作用の実行(ロギング等) |
| map |
レスポンスデータの変換 |
| catchError |
エラーのキャッチと変換 |
| timeout |
タイムアウト処理の追加 |
| finalize |
完了時のクリーンアップ処理 |
Interceptorを適切に活用することで、以下のメリットが得られます。
- ビジネスロジックと横断的関心事の分離
- 再利用可能なコードの作成
- 統一されたレスポンス形式の実現
- パフォーマンス計測やロギングの一元化
次のステップとして、Exception Filterによる統一されたエラーハンドリングの実装を学ぶことで、NestJSのリクエスト処理パイプラインの理解がさらに深まります。
参考リンク#