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]
    end

RxJSオペレータの組み合わせパターン

複数のオペレータを組み合わせて、より複雑な処理を実現できます。

ロギング + 変換 + タイムアウトの複合パターン

 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のリクエスト処理パイプラインの理解がさらに深まります。

参考リンク