T-CREATOR

NestJS Guard/Interceptor/Filter 早見表:適用順序とユースケース対応表

NestJS Guard/Interceptor/Filter 早見表:適用順序とユースケース対応表

NestJS で API を開発していると、「この処理は Guard で書くべき?それとも Interceptor?」と迷った経験はありませんか?リクエストライフサイクルの中で、Guard、Interceptor、Filter はそれぞれ異なる役割を持っており、適切に使い分けることで保守性の高いコードを実現できます。本記事では、これら 3 つのコンポーネントの適用順序とユースケースを早見表とともに解説し、実務ですぐに活用できる知識をお届けします。

早見表:Guard/Interceptor/Filter 比較

基本特性比較表

#コンポーネント実行タイミング主な役割戻り値エラー時の動作
1Guardコントローラー実行前認証・認可チェックboolean / Promise<boolean>false 返却でリクエスト拒否
2Interceptorコントローラー前後リクエスト/レスポンス変換Observable例外スロー可能
3Filter例外発生時エラーハンドリングレスポンス整形クライアントへ返却

ユースケース対応表

#ユースケース推奨コンポーネント理由
1JWT 認証チェックGuardリクエスト処理前に実行すべき
2ロール基盤アクセス制御Guard認可判定に特化
3リクエストログ記録Interceptor実行前後の情報取得可能
4レスポンスデータ変換Interceptor戻り値を加工できる
5実行時間計測Interceptor前後処理のタイミング取得
6HTTP エラーレスポンス統一Filter例外を統一フォーマットへ変換
7バリデーションエラー処理Filter422 エラーの詳細情報整形
8データベースエラー処理Filter500 エラーの安全な返却

適用順序表

#順序コンポーネント処理内容次のステップへの条件
11MiddlewareCORS、ロギングなど常に次へ
22Guard認証・認可チェックtrue の場合のみ
33Interceptor (Before)リクエスト前処理常に次へ
44Pipeバリデーション・変換成功時のみ
55Controller Handlerビジネスロジック実行正常時のみ
66Interceptor (After)レスポンス後処理常に実行
77Filter例外発生時のみエラー時のみ

背景

NestJS のリクエストライフサイクル

NestJS は、Express(または Fastify)をベースとした Node.js フレームワークで、エンタープライズグレードのアプリケーション開発に必要な機能を標準で提供しています。その中核となるのが、リクエストからレスポンスまでの処理を段階的に制御できるライフサイクル機構です。

クライアントからリクエストが送信されると、NestJS は定義された順序で各コンポーネントを実行していきます。この仕組みにより、横断的関心事(認証、ログ、エラー処理など)をビジネスロジックから分離でき、コードの再利用性と保守性が大幅に向上するのです。

下記の図は、リクエストが到達してからレスポンスが返却されるまでの基本的な流れを示しています。

mermaidflowchart TD
    A["クライアント"] -->|"HTTPリクエスト"| B["Middleware"]
    B --> C["Guard"]
    C -->|"true"| D["Interceptor<br/>(Before)"]
    C -->|"false"| K["403 Forbidden"]
    D --> E["Pipe"]
    E --> F["Controller<br/>Handler"]
    F --> G["Interceptor<br/>(After)"]
    G --> H["クライアント"]
    F -.->|"例外発生"| I["Exception Filter"]
    E -.->|"バリデーション失敗"| I
    I --> J["エラー<br/>レスポンス"]
    J --> H
    K --> H

図のポイント:実線は正常フロー、点線はエラー時のフローを表します。Guard で認証失敗するとそこで処理が止まり、例外が発生すると Filter が処理を引き継ぎます。

各コンポーネントの設計思想

NestJS の設計思想は、関心の分離(Separation of Concerns)に基づいています。Guard、Interceptor、Filter はそれぞれ異なる責務を持ち、組み合わせることで複雑な要件に対応できるのです。

Guard は「このリクエストを処理してもいいか?」という判断に特化しています。認証トークンの検証やロールベースのアクセス制御など、リクエストを受け入れるかどうかの門番としての役割を果たすのです。

Interceptor は「リクエスト/レスポンスをどう加工するか?」に焦点を当てます。ログ記録、データ変換、キャッシュ制御など、処理の前後で横断的に実行したい処理を実装するのに最適でしょう。

Filter は「エラーが発生したらどう対処するか?」を担当します。予期しない例外をキャッチし、クライアントに適切なエラーレスポンスを返す最後の砦として機能するのです。

課題

適用箇所の判断に迷う問題

NestJS 初心者がつまずきやすいのが、「この処理をどのコンポーネントに実装すべきか」という判断です。例えば、ユーザー情報のログ記録は、Guard でも Interceptor でも技術的には実現できます。しかし、適切でない場所に実装すると、後々の保守で苦労することになるでしょう。

認証チェックを Interceptor で実装してしまうケースもよく見られます。一見動作するものの、エラーハンドリングが複雑になり、コードの可読性が低下してしまうのです。

実行順序の理解不足

各コンポーネントがどの順序で実行されるかを理解していないと、予期しない動作に遭遇します。例えば、Interceptor でリクエストデータをログと Go したつもりが、実際には Guard で弾かれてログが記録されないといった問題が発生するのです。

特に、グローバル、コントローラー、メソッドの各レベルでコンポーネントを適用した場合、実行順序がさらに複雑になります。ドキュメントを読んでも、実際の挙動をイメージしにくいという声を多く耳にするでしょう。

エラー処理の一貫性欠如

アプリケーション全体でエラーレスポンスの形式が統一されていないと、クライアント側の実装が煩雑になります。バリデーションエラーは 422、認証エラーは 401、予期しないエラーは 500 と、HTTP ステータスコードは使い分けられても、レスポンスボディの構造がバラバラでは使いにくいですね。

Filter を適切に使えば、すべての例外を統一フォーマットで返却できます。しかし、どのレベルで Filter を適用すべきか、既存の NestJS 組み込み Filter との関係をどう扱うかなど、設計判断が必要になるのです。

下記の図は、コンポーネント選択を誤った場合の問題を示しています。

mermaidflowchart LR
    A["要件"] --> B{"コンポーネント<br/>選択"}
    B -->|"誤った選択"| C["Guard"]
    B -->|"誤った選択"| D["Interceptor"]
    B -->|"誤った選択"| E["Filter"]
    C --> F["保守性低下"]
    C --> G["エラー処理複雑化"]
    D --> H["責務の混在"]
    D --> I["テスト困難"]
    E --> J["過剰な例外キャッチ"]
    E --> K["パフォーマンス低下"]

図の要点:適切でないコンポーネントに処理を実装すると、保守性やテスト容易性が損なわれ、長期的な開発効率に悪影響を与えます。

解決策

Guard:認証・認可の門番として活用

Guard は、CanActivateインターフェースを実装したクラスで、canActivateメソッドが true を返した場合のみリクエスト処理が続行されます。認証トークンの検証、ユーザーのロール確認など、「このリクエストを処理してよいか」の判断に特化させましょう。

Guard の最大の特徴は、実行コンテキスト(ExecutionContext)にアクセスできることです。これにより、リクエストオブジェクトやメタデータ(デコレーターで付与した情報)を取得して、柔軟な判断が可能になります。

実装時のポイントは、Guard を可能な限りシンプルに保つことです。複雑なビジネスロジックを Guard に含めると、テストが困難になり、再利用性も低下してしまうでしょう。

Interceptor:横断的処理の中心的存在

Interceptor は、NestInterceptorインターフェースを実装し、interceptメソッドで RxJS の Observable を返却します。この仕組みにより、リクエスト処理の前後で自由に処理を挟み込めるのです。

ログ記録、レスポンスデータの変換、実行時間の計測、キャッシュ制御など、ビジネスロジックとは独立した横断的関心事の実装に最適でしょう。RxJS のオペレーター(map、tap、catchError など)を活用することで、宣言的で読みやすいコードになります。

Interceptor の強力な点は、チェーン可能であることです。複数の Interceptor を組み合わせることで、小さな責務に分割された再利用可能なコンポーネントを構築できます。

Filter:統一されたエラーレスポンス

Exception Filter は、ExceptionFilterインターフェースを実装し、catchメソッドで例外をキャッチしてレスポンスを生成します。NestJS が標準で提供するHttpExceptionだけでなく、カスタム例外やデータベースエラーなども処理できるのです。

すべての Filter を 1 つのクラスに集約するのではなく、エラーの種類ごとに Filter を分割する設計も有効でしょう。例えば、ValidationExceptionFilterDatabaseExceptionFilterを分けることで、各 Filter の責務が明確になります。

グローバル Filter として登録すれば、アプリケーション全体で一貫したエラーレスポンスフォーマットを実現できます。これにより、フロントエンド開発者がエラー処理を実装しやすくなるのです。

下記の図は、各コンポーネントの責務分担を示しています。

mermaidflowchart TB
    subgraph Request["リクエスト処理フロー"]
        A["リクエスト到達"]
        B["Guard<br/>認証・認可判定"]
        C["Interceptor Before<br/>前処理"]
        D["Controller<br/>ビジネスロジック"]
        E["Interceptor After<br/>後処理"]
        F["レスポンス返却"]
    end

    subgraph Error["エラー処理フロー"]
        G["例外発生"]
        H["Filter<br/>エラーハンドリング"]
        I["エラーレスポンス"]
    end

    A --> B
    B -->|"OK"| C
    B -->|"NG"| G
    C --> D
    D --> E
    E --> F
    D -.->|"例外"| G
    G --> H
    H --> I

図で理解できる要点

  • Guard は入口での判定、Interceptor は前後処理、Filter はエラー時の処理と、明確に役割が分かれています
  • 正常フローとエラーフローが分離されており、コードの見通しが良くなります

適用レベルと実行順序の制御

各コンポーネントは、グローバル、コントローラー、メソッドの 3 つのレベルで適用できます。実行順序は、グローバル → コントローラー → メソッドの順です。

例えば、グローバルに JWT 認証 Guard を適用し、特定のエンドポイントだけ管理者ロールチェック Guard を追加するといった設計が可能になります。この階層的な適用により、共通処理と個別処理を自然に分離できるのです。

注意点として、同じレベルに複数のコンポーネントを適用した場合、登録順に実行されます。そのため、依存関係がある場合は、登録順序を意識する必要があるでしょう。

具体例

JWT 認証 Guard の実装

JWT 認証は、NestJS アプリケーションで最も一般的な認証方式の 1 つです。Guard を使ってトークンを検証し、有効なユーザーのみリクエスト処理を継続させる実装を見ていきましょう。

パッケージのインストール

まず、JWT 処理に必要なパッケージをインストールします。

bashyarn add @nestjs/jwt @nestjs/passport passport passport-jwt
yarn add -D @types/passport-jwt

JWT 認証 Guard の基本構造

Guard クラスはCanActivateインターフェースを実装します。canActivateメソッドで認証ロジックを実装しましょう。

typescriptimport {
  Injectable,
  CanActivate,
  ExecutionContext,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  async canActivate(
    context: ExecutionContext
  ): Promise<boolean> {
    // ExecutionContextからHTTPリクエストを取得
    const request = context.switchToHttp().getRequest();

    // 次のステップで実装
    return true;
  }
}

このコードでは、ExecutionContextを使って HTTP リクエストオブジェクトにアクセスしています。switchToHttp()は HTTP 固有のコンテキストに切り替えるメソッドです。

トークンの抽出と検証

Authorization ヘッダーからトークンを抽出し、JWTService で検証します。

typescriptasync canActivate(context: ExecutionContext): Promise<boolean> {
  const request = context.switchToHttp().getRequest();

  // Authorizationヘッダーからトークンを抽出
  const token = this.extractTokenFromHeader(request);
  if (!token) {
    throw new UnauthorizedException('トークンが見つかりません');
  }

  try {
    // JWTトークンを検証してペイロードを取得
    const payload = await this.jwtService.verifyAsync(token, {
      secret: process.env.JWT_SECRET,
    });

    // リクエストオブジェクトにユーザー情報を格納
    request['user'] = payload;
  } catch {
    throw new UnauthorizedException('無効なトークンです');
  }

  return true;
}

verifyAsyncメソッドがトークンの有効性を検証し、成功するとペイロード(ユーザー情報など)を返却します。検証失敗時は例外がスローされ、それをキャッチしてUnauthorizedExceptionを投げています。

トークン抽出ヘルパーメソッド

Authorization ヘッダーから「Bearer」プレフィックスを除いてトークンを取り出す処理です。

typescriptprivate extractTokenFromHeader(request: Request): string | undefined {
  const authorization = request.headers['authorization'];
  if (!authorization) {
    return undefined;
  }

  // "Bearer <token>" の形式からトークン部分のみを抽出
  const [type, token] = authorization.split(' ');
  return type === 'Bearer' ? token : undefined;
}

Guard の適用方法

コントローラーで Guard を適用します。@UseGuardsデコレーターを使用するのです。

typescriptimport { Controller, Get, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from './guards/jwt-auth.guard';

@Controller('users')
export class UsersController {
  // このエンドポイントはJWT認証が必要
  @Get('profile')
  @UseGuards(JwtAuthGuard)
  getProfile(@Request() req) {
    // Guardでrequest['user']に格納したユーザー情報を利用
    return req.user;
  }
}

メソッドレベルで適用すれば、特定のエンドポイントだけ認証を要求できます。コントローラークラスに適用すれば、すべてのエンドポイントで認証が必要になるのです。

ロール基盤アクセス制御 Guard の実装

JWT 認証に加えて、ユーザーのロール(管理者、一般ユーザーなど)による制御を実装しましょう。カスタムデコレーターと Reflector を組み合わせます。

ロール定義とカスタムデコレーター

まず、ロールを定義し、メタデータとして付与するデコレーターを作成します。

typescript// roles.enum.ts
export enum Role {
  User = 'user',
  Admin = 'admin',
  Moderator = 'moderator',
}
typescript// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { Role } from './roles.enum';

// メタデータのキー
export const ROLES_KEY = 'roles';

// カスタムデコレーター:必要なロールを設定
export const Roles = (...roles: Role[]) =>
  SetMetadata(ROLES_KEY, roles);

SetMetadataは、エンドポイントにメタデータを付与する NestJS 標準の関数です。このメタデータを Guard 内で取得して判定に使用します。

RolesGuard の実装

Reflector を使ってメタデータを取得し、ユーザーのロールと比較します。

typescriptimport {
  Injectable,
  CanActivate,
  ExecutionContext,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
import { Role } from './roles.enum';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    // メタデータから必要なロールを取得
    const requiredRoles = this.reflector.getAllAndOverride<
      Role[]
    >(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    // ロール指定がなければ、アクセス許可
    if (!requiredRoles) {
      return true;
    }

    // 次のステップで実装
    return false;
  }
}

getAllAndOverrideは、メソッドレベルとクラスレベルのメタデータを取得し、メソッドレベルが優先されます。これにより、コントローラー全体とメソッド個別で異なるロール要件を設定できるのです。

ユーザーロールの検証

リクエストオブジェクトからユーザー情報を取得し、ロールを検証します。

typescriptcanActivate(context: ExecutionContext): boolean {
  const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
    context.getHandler(),
    context.getClass(),
  ]);

  if (!requiredRoles) {
    return true;
  }

  // JwtAuthGuardで格納されたユーザー情報を取得
  const { user } = context.switchToHttp().getRequest();

  // ユーザーのロールが必要なロールに含まれているか判定
  return requiredRoles.some((role) => user.roles?.includes(role));
}

someメソッドで、ユーザーが持つロールのいずれかが必要なロールに含まれているかをチェックしています。複数ロールの OR 条件になるのです。

コントローラーでの使用例

JwtAuthGuard と RolesGuard を組み合わせて使います。

typescriptimport {
  Controller,
  Get,
  Post,
  UseGuards,
} from '@nestjs/common';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { RolesGuard } from './guards/roles.guard';
import { Roles } from './decorators/roles.decorator';
import { Role } from './enums/roles.enum';

@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard) // 両方のGuardを適用
export class AdminController {
  // 管理者のみアクセス可能
  @Get('users')
  @Roles(Role.Admin)
  getAllUsers() {
    return 'すべてのユーザー情報';
  }

  // 管理者またはモデレーターがアクセス可能
  @Post('approve')
  @Roles(Role.Admin, Role.Moderator)
  approveContent() {
    return 'コンテンツを承認しました';
  }
}

複数の Guard を指定した場合、記述順に実行されます。JwtAuthGuard→RolesGuard の順で実行され、どちらかが false を返すとリクエストは拒否されるのです。

ロギング Interceptor の実装

リクエストの詳細情報と実行時間をログに記録する Interceptor を実装しましょう。RxJS のオペレーターを活用します。

基本的なロギング Interceptor

NestInterceptorインターフェースを実装し、interceptメソッドを定義します。

typescriptimport {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  Logger,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger(
    LoggingInterceptor.name
  );

  intercept(
    context: ExecutionContext,
    next: CallHandler
  ): Observable<any> {
    // リクエスト情報を取得
    const request = context.switchToHttp().getRequest();
    const { method, url } = request;

    // 次のステップで実装
    return next.handle();
  }
}

CallHandlerhandle()メソッドを呼び出すと、次の処理(コントローラーハンドラー)が実行され、結果が Observable として返却されます。

実行時間の計測

リクエスト受信時刻を記録し、レスポンス返却時に経過時間を計算します。

typescriptintercept(context: ExecutionContext, next: CallHandler): Observable<any> {
  const request = context.switchToHttp().getRequest();
  const { method, url, ip } = request;
  const userAgent = request.get('user-agent') || '';

  // リクエスト受信時刻を記録
  const now = Date.now();

  this.logger.log(
    `[Request] ${method} ${url} - ${ip} - ${userAgent}`
  );

  // RxJSのtapオペレーターで、レスポンス返却時に処理を実行
  return next.handle().pipe(
    tap(() => {
      const responseTime = Date.now() - now;
      this.logger.log(
        `[Response] ${method} ${url} - ${responseTime}ms`
      );
    }),
  );
}

tapオペレーターは、Observable のデータストリームに副作用を与えずに処理を実行できます。ログ記録に最適なオペレーターですね。

エラー時のログ記録

エラーが発生した場合も、ログに記録するようにします。

typescriptimport { tap, catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
  const request = context.switchToHttp().getRequest();
  const { method, url, ip } = request;
  const now = Date.now();

  this.logger.log(`[Request] ${method} ${url} - ${ip}`);

  return next.handle().pipe(
    tap(() => {
      const responseTime = Date.now() - now;
      this.logger.log(`[Response] ${method} ${url} - ${responseTime}ms`);
    }),
    catchError((error) => {
      const responseTime = Date.now() - now;
      this.logger.error(
        `[Error] ${method} ${url} - ${responseTime}ms - ${error.message}`
      );
      // エラーを再スロー(Filterで処理されるように)
      return throwError(() => error);
    }),
  );
}

catchErrorでエラーをキャッチし、ログ記録後にthrowErrorで再スローします。これにより、後続の Filter でエラーハンドリングが継続されるのです。

グローバル適用

main.ts でアプリケーション全体に適用します。

typescript// 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);

  // すべてのエンドポイントにLoggingInterceptorを適用
  app.useGlobalInterceptors(new LoggingInterceptor());

  await app.listen(3000);
}
bootstrap();

グローバル Interceptor は、依存性注入が効かないため、Logger などのサービスを直接インポートして使用する必要がある点に注意しましょう。

レスポンス変換 Interceptor の実装

API のレスポンスを統一フォーマットに変換する Interceptor です。成功レスポンスを常に同じ構造で返却できます。

レスポンスインターフェースの定義

統一フォーマットの型を定義します。

typescript// interfaces/response.interface.ts
export interface ApiResponse<T> {
  success: boolean;
  data: T;
  message: string;
  timestamp: string;
}

すべてのレスポンスがこの構造に従うことで、フロントエンド側の処理が統一できます。

TransformInterceptor の実装

RxJS のmapオペレーターでレスポンスデータを変換します。

typescriptimport {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiResponse } from './interfaces/response.interface';

@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,
        message: 'リクエストが正常に処理されました',
        timestamp: new Date().toISOString(),
      }))
    );
  }
}

mapオペレーターは、Observable のデータを変換します。コントローラーが返したデータをdataプロパティに格納し、その他のメタ情報を追加しているのです。

コントローラーでの使用

コントローラーは通常通りデータを返せば、自動的に変換されます。

typescript@Controller('products')
@UseInterceptors(TransformInterceptor)
export class ProductsController {
  @Get()
  findAll() {
    // 配列をそのまま返す
    return [
      { id: 1, name: '商品A', price: 1000 },
      { id: 2, name: '商品B', price: 2000 },
    ];
  }
}

このコードの実際のレスポンスは以下のようになります。

json{
  "success": true,
  "data": [
    { "id": 1, "name": "商品A", "price": 1000 },
    { "id": 2, "name": "商品B", "price": 2000 }
  ],
  "message": "リクエストが正常に処理されました",
  "timestamp": "2025-01-15T10:30:00.000Z"
}

コントローラーのコードがシンプルになり、レスポンス構造の一貫性が保たれます。

HTTP 例外 Filter の実装

すべての HTTP 例外を統一フォーマットで返却する Filter を実装しましょう。クライアント側のエラー処理が簡素化されます。

基本的な HttpExceptionFilter

ExceptionFilterインターフェースを実装します。

typescriptimport {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
} from '@nestjs/common';
import { Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter
  implements ExceptionFilter
{
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    // 次のステップで実装
  }
}

@Catch(HttpException)デコレーターで、この Filter がキャッチする例外の型を指定します。HttpExceptionとそのサブクラスすべてがキャッチ対象になるのです。

エラーレスポンスの生成

HTTP ステータスコードとエラー詳細を取得し、統一フォーマットで返却します。

typescriptcatch(exception: HttpException, host: ArgumentsHost) {
  const ctx = host.switchToHttp();
  const response = ctx.getResponse<Response>();
  const request = ctx.getRequest<Request>();

  const status = exception.getStatus();
  const exceptionResponse = exception.getResponse();

  // エラーメッセージの抽出
  const message =
    typeof exceptionResponse === 'string'
      ? exceptionResponse
      : (exceptionResponse as any).message || '予期しないエラーが発生しました';

  // 統一フォーマットでレスポンス
  response.status(status).json({
    success: false,
    statusCode: status,
    message: message,
    timestamp: new Date().toISOString(),
    path: request.url,
  });
}

getResponse()で取得できるデータは、例外の種類によって構造が異なります。文字列の場合とオブジェクトの場合を考慮して、安全にメッセージを取り出しているのです。

バリデーションエラーの詳細表示

ValidationPipeによるバリデーションエラーは、詳細情報を含んでいます。これをクライアントに返却しましょう。

typescriptcatch(exception: HttpException, host: ArgumentsHost) {
  const ctx = host.switchToHttp();
  const response = ctx.getResponse<Response>();
  const request = ctx.getRequest<Request>();
  const status = exception.getStatus();
  const exceptionResponse = exception.getResponse();

  // バリデーションエラーの詳細を抽出
  let errors = null;
  if (status === 400 && typeof exceptionResponse === 'object') {
    errors = (exceptionResponse as any).message || null;
  }

  response.status(status).json({
    success: false,
    statusCode: status,
    message: Array.isArray(errors) ? 'バリデーションエラー' : errors || '予期しないエラー',
    errors: Array.isArray(errors) ? errors : undefined,
    timestamp: new Date().toISOString(),
    path: request.url,
  });
}

バリデーションエラー時は、errors配列に各フィールドのエラーメッセージが格納されます。クライアント側でフィールドごとのエラー表示が可能になるのです。

全例外 Filter の実装

HttpException以外の予期しない例外(データベースエラー、ランタイムエラーなど)もキャッチする包括的な Filter です。

AllExceptionsFilter の実装

すべての例外をキャッチし、安全なエラーレスポンスを返却します。

typescriptimport {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
  Logger,
} from '@nestjs/common';

@Catch()
export class AllExceptionsFilter
  implements ExceptionFilter
{
  private readonly logger = new Logger(
    AllExceptionsFilter.name
  );

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

    // 次のステップで実装
  }
}

@Catch()(引数なし)とすることで、すべての例外がキャッチ対象になります。

HttpException と一般例外の判別

例外の型に応じて、適切なステータスコードとメッセージを設定します。

typescriptcatch(exception: unknown, host: ArgumentsHost) {
  const ctx = host.switchToHttp();
  const response = ctx.getResponse();
  const request = ctx.getRequest();

  let status = HttpStatus.INTERNAL_SERVER_ERROR;
  let message = '内部サーバーエラーが発生しました';

  if (exception instanceof HttpException) {
    status = exception.getStatus();
    const exceptionResponse = exception.getResponse();
    message =
      typeof exceptionResponse === 'string'
        ? exceptionResponse
        : (exceptionResponse as any).message;
  } else if (exception instanceof Error) {
    message = exception.message;
  }

  // エラー詳細をログに記録
  this.logger.error(
    `Status: ${status}, Message: ${message}`,
    exception instanceof Error ? exception.stack : ''
  );

  // 次のステップで実装
}

instanceofで例外の型を判別し、適切に処理しています。一般的なErrorの場合もmessageプロパティを取得できるのです。

本番環境とヴ発環境での出し分け

本番環境では、セキュリティのためエラー詳細を隠蔽します。

typescriptcatch(exception: unknown, host: ArgumentsHost) {
  // ... (前のステップのコード)

  const isDevelopment = process.env.NODE_ENV === 'development';

  response.status(status).json({
    success: false,
    statusCode: status,
    message: isDevelopment ? message : 'エラーが発生しました',
    // 開発環境のみスタックトレースを含める
    stack: isDevelopment && exception instanceof Error ? exception.stack : undefined,
    timestamp: new Date().toISOString(),
    path: request.url,
  });
}

本番環境では詳細なエラーメッセージやスタックトレースを返さないことで、システムの内部構造が漏洩するリスクを減らせます。

main.ts でのグローバル適用

すべての例外をキャッチするため、アプリケーション全体に適用します。

typescript// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AllExceptionsFilter } from './filters/all-exceptions.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // グローバルにAllExceptionsFilterを適用
  app.useGlobalFilters(new AllExceptionsFilter());

  await app.listen(3000);
}
bootstrap();

グローバル Filter は、モジュール内の依存性注入が使えない点に注意が必要です。依存性注入を使いたい場合は、AppModule の providers に登録する方法もあります。

実行順序の確認テスト

実際にコードを動かして、Guard、Interceptor、Filter の実行順序を確認してみましょう。ログ出力で視覚化します。

テスト用 Guard

実行タイミングをログに記録するシンプルな Guard です。

typescriptimport {
  Injectable,
  CanActivate,
  ExecutionContext,
  Logger,
} from '@nestjs/common';

@Injectable()
export class TestGuard implements CanActivate {
  private readonly logger = new Logger('TestGuard');

  canActivate(context: ExecutionContext): boolean {
    this.logger.log('🛡️ Guard: 実行中');
    return true; // 常にtrueを返してリクエストを許可
  }
}

テスト用 Interceptor

前処理と後処理のタイミングをログに記録します。

typescriptimport {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  Logger,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class TestInterceptor implements NestInterceptor {
  private readonly logger = new Logger('TestInterceptor');

  intercept(
    context: ExecutionContext,
    next: CallHandler
  ): Observable<any> {
    this.logger.log('🔄 Interceptor: 前処理');

    return next.handle().pipe(
      tap(() => {
        this.logger.log('🔄 Interceptor: 後処理');
      })
    );
  }
}

テスト用コントローラー

Guard と Interceptor を適用したエンドポイントを用意します。

typescriptimport {
  Controller,
  Get,
  UseGuards,
  UseInterceptors,
  Logger,
} from '@nestjs/common';
import { TestGuard } from './test.guard';
import { TestInterceptor } from './test.interceptor';

@Controller('test')
export class TestController {
  private readonly logger = new Logger('TestController');

  @Get('order')
  @UseGuards(TestGuard)
  @UseInterceptors(TestInterceptor)
  testOrder() {
    this.logger.log('🎯 Controller: ハンドラー実行');
    return { message: '実行順序テスト' };
  }

  @Get('error')
  @UseGuards(TestGuard)
  @UseInterceptors(TestInterceptor)
  testError() {
    this.logger.log('🎯 Controller: エラーをスロー');
    throw new Error('テスト用エラー');
  }
}

コンソール出力の確認

正常時とエラー時のログ出力を比較すると、実行順序が明確になります。

正常時(GET /test/order)のログ:

yaml🛡️ Guard: 実行中
🔄 Interceptor: 前処理
🎯 Controller: ハンドラー実行
🔄 Interceptor: 後処理

エラー時(GET /test/error)のログ:

yaml🛡️ Guard: 実行中
🔄 Interceptor: 前処理
🎯 Controller: エラーをスロー
[Error] 例外Filterが実行されます

エラー時は、Interceptor の後処理がスキップされ、直接 Filter に移行することがわかります。この挙動を理解しておくと、ログ記録や後処理の設計に役立つでしょう。

下記の図は、実際の実行フローとログ出力の関係を示しています。

mermaidsequenceDiagram
    participant Client as クライアント
    participant Guard as Guard
    participant Inter as Interceptor
    participant Ctrl as Controller
    participant Filter as Filter

    Client->>Guard: リクエスト
    Note over Guard: 🛡️ Guard実行
    Guard->>Inter: 認証OK
    Note over Inter: 🔄 前処理
    Inter->>Ctrl: 次へ
    Note over Ctrl: 🎯 ハンドラー実行

    alt 正常終了
        Ctrl->>Inter: レスポンス
        Note over Inter: 🔄 後処理
        Inter->>Client: 返却
    else エラー発生
        Ctrl->>Filter: 例外スロー
        Note over Filter: 🚨 Filter実行
        Filter->>Client: エラーレスポンス
    end

図で理解できる要点

  • 正常フローでは、Guard→Interceptor 前処理 →Controller→Interceptor 後処理の順に実行されます
  • エラー発生時は、Interceptor 後処理をスキップして Filter に直接移行します
  • この動作を理解すると、後処理が必須の場合は Interceptor の catchError で対応する必要があることがわかります

まとめ

NestJS の Guard、Interceptor、Filter は、それぞれ明確な役割と適用タイミングを持つコンポーネントです。Guard は認証・認可の門番として、Interceptor はリクエスト/レスポンスの加工役として、Filter はエラー処理の最終防衛ラインとして機能します。

適用順序を理解し、各コンポーネントの責務を明確にすることで、保守性が高く拡張しやすいアプリケーション設計が実現できるでしょう。早見表を参照しながら、適切なコンポーネントを選択してください。

実装時のポイントは、可能な限りシンプルに保つことです。1 つの Guard や Interceptor に複数の責務を詰め込むと、テストが困難になり再利用性も低下してしまいます。小さな責務に分割し、組み合わせて使う設計を心がけましょう。

グローバル適用とメソッド/コントローラーレベルの適用を使い分けることで、共通処理と個別処理を自然に分離できます。階層的な設計により、コードの重複を避けつつ、柔軟な制御が可能になるのです。

エラーハンドリングは、Filter をグローバル Β に適用して統一フォーマットで返却することをお勧めします。フロントエンド開発者がエラー処理を実装しやすくなり、開発効率が向上するでしょう。

本記事で紹介した実装パターンを、ぜひ実際のプロジェクトで活用してみてください。適切なコンポーネント選択により、可読性と保守性の高い NestJS アプリケーションを構築できるはずです。

関連リンク