目次を表示する

NestJS Deep Dive 2026 ─ 内部構造・再発明回避・パフォーマンスを 10 章で読み解く

リクエストライフサイクル ─ 6 つの層を正しく使い分ける

第3章: リクエストライフサイクル ─ 6 つの層を正しく使い分ける

リクエストライフサイクル 6 層フロー

第1章で起動時、第2章で Decorator を扱った。本章は実行時 ─ リクエストが来てからレスポンスを返すまでに NestJS が走らせる 6 つの層を分解する。

「どこで何をすべきか」を間違えると、第9章で扱うアンチパターンの大半が生まれる。Interceptor で DB を引く / Pipe で外部 API を叩く / Service 内に try-catch を散らかす ─ これらは全て、層の役割を取り違えた結果だ。

完全な順序

NestJS のリクエストパイプラインは以下の順序で動く。

graph TB
    Req[Incoming Request] --> M[1. Middleware<br/>global → module-bound]
    M --> G[2. Guards<br/>global → controller → route]
    G --> IB[3. Interceptors before<br/>global → controller → route]
    IB --> P[4. Pipes<br/>global → controller → route → 引数<br/>※引数は last → first]
    P --> H[5. Handler]
    H --> IA[6. Interceptors after<br/>route → controller → global<br/>※first-in-last-out]
    IA --> EF{例外?}
    EF -->|Yes| F[Exception Filter<br/>route → controller → global]
    EF -->|No| Res[Outgoing Response]
    F --> Res
    style M fill:#1a2030,stroke:#7c8db5
    style G fill:#1a2030,stroke:#ff4d6d
    style IB fill:#1a2030,stroke:#b794f4
    style P fill:#1a2030,stroke:#4cc9f0
    style H fill:#1a2030,stroke:#00d9c0
    style IA fill:#1a2030,stroke:#b794f4
    style F fill:#1a2030,stroke:#ff6b35

Interceptor before」と「Interceptor after」は同じ Interceptor の intercept() メソッドの前半と後半だ。RxJS の Observable を使って handler を「囲む」構造になっている。

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Before...');                                  // ← Interceptor before
    const start = Date.now();
    return next.handle().pipe(
      tap(() => console.log(`After... ${Date.now() - start}ms`)) // ← Interceptor after
    );
  }
}

各層の責務 ─ 「ここで何をすべきか」

責務やってよいことやってはいけないこと
Middlewareプラットフォーム依存の前処理ロギング、ヘッダ操作、CORSビジネスロジック、エラー整形
Guard認可・認証boolean を返して通過/拒否データ取得(Service / Repository に)
Interceptor (before)実行前共通処理タイマー開始、セッション解決DB アクセス、外部 API 呼び出し
Pipeデータ検証・変換class-validator / transform副作用のある処理、外部 API
HandlerビジネスロジックService の呼び出し、結果返却横断関心事(log/cache/auth)
Interceptor (after)レスポンス変換、tap 系map / tap でログ・キャッシュDB アクセス、レスポンスの再 fetch
Exception Filter例外整形エラーを HTTP レスポンスに変換ビジネスロジック、リトライ

エラー時の流れ

任意のステージ(Middleware, Guard, Interceptor, Pipe, Handler, Service)で throw されると、ExceptionsHandlerlowest scope の Filter から順 に探して match した filter で処理する。

graph LR
    Throw[throw new HttpException]
    Throw --> R{route Filter?}
    R -->|hit| Done1[整形して response]
    R -->|miss| C{controller Filter?}
    C -->|hit| Done2[整形して response]
    C -->|miss| G{global Filter?}
    G -->|hit| Done3[整形して response]
    G -->|miss| Default[ビルトインの error response]

ここで重要なのは、Service 内で try-catch を書きまくらないということ。NestJS の哲学は「throw すれば Filter が拾う」だ。

// ❌ 悪い:Service 内で try-catch、独自エラー整形
@Injectable()
export class CatsService {
  async findById(id: number) {
    try {
      const cat = await this.repo.findOne(id);
      if (!cat) {
        return { error: 'NOT_FOUND', message: '...' }; // ← 戻り値で表現
      }
      return cat;
    } catch (e) {
      return { error: 'INTERNAL', message: e.message }; // ← 戻り値で表現
    }
  }
}

// ✅ 良い:throw に統一、Filter で一括整形
@Injectable()
export class CatsService {
  async findById(id: number) {
    const cat = await this.repo.findOne(id);
    if (!cat) throw new NotFoundException(`Cat ${id} not found`);
    return cat;
  }
}

各層のスコープ ─ global / controller / route

各層は 3 つのスコープで適用できる。global が最も粗く、route が最も細かい。

// 1. Global - 全エンドポイントに適用
app.useGlobalGuards(new AuthGuard()); // ← DI が効かない!
// または
{ provide: APP_GUARD, useClass: AuthGuard }; // ← DI が効く(推奨)

// 2. Controller - 特定 Controller のすべての route
@UseGuards(AuthGuard)
@Controller('cats')
export class CatsController {}

// 3. Route - 特定 method のみ
@UseGuards(AuthGuard)
@Get(':id')
findOne() {}

useGlobalGuards(new AuthGuard()) の落とし穴

useGlobalGuards()インスタンスを渡すと、そのインスタンスは DI コンテナの外で生成される。Guard 内で他の Service を inject できない。

// ❌ 悪い:DI 外でインスタンス生成
app.useGlobalGuards(new AuthGuard()); // ← AuthGuard は ConfigService 等を注入できない

// ✅ 良い:APP_GUARD token で provider 登録
@Module({
  providers: [
    { provide: APP_GUARD, useClass: AuthGuard }, // ← DI ツリーに乗る
  ],
})
export class AppModule {}

これは Pipe / Interceptor / Filter でも同じ。APP_PIPE / APP_INTERCEPTOR / APP_FILTER が用意されている。実装で DI を使うなら絶対にこちらを使う

Pipe ─ 引数ごとの動作

Pipe は他の層と違い、引数ごとに last → first の順で適用される。

@Get(':id/:type')
findOne(
  @Param('id', ParseIntPipe) id: number,        // ← 第1引数(最後に処理される)
  @Param('type', new ValidatePipe()) type: string, // ← 第2引数(先に処理される)
) {}

これは内部的に「右から左へ」処理されるという RxJS / 関数合成の伝統に従う。

ただし、Pipe で副作用のある処理を書いてはいけない

// ❌ 悪い:Pipe で外部 API 呼び出し
@Injectable()
export class FetchUserPipe implements PipeTransform {
  async transform(value: number) {
    return await fetch(`/api/users/${value}`).then(r => r.json()); // ← Pipe で外部呼び出し
  }
}

// ✅ 良い:Pipe は変換のみ、外部呼び出しは Service / Interceptor に
@Injectable()
export class ParseIdPipe implements PipeTransform {
  transform(value: string): number {
    const id = parseInt(value);
    if (isNaN(id)) throw new BadRequestException('id must be a number');
    return id;
  }
}

Interceptor の重ね順と RxJS

Interceptor は first-in-last-out で重なる。最後に登録された Interceptor の tap最初に実行される。

// app.module.ts
{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
{ provide: APP_INTERCEPTOR, useClass: TransformInterceptor },
graph TB
    Req[Request] --> L1[LoggingInterceptor before]
    L1 --> T1[TransformInterceptor before]
    T1 --> H[Handler]
    H --> T2[TransformInterceptor after]
    T2 --> L2[LoggingInterceptor after]
    L2 --> Res[Response]
    style L1 fill:#1a2030,stroke:#b794f4
    style T1 fill:#1a2030,stroke:#4cc9f0
    style H fill:#1a2030,stroke:#00d9c0
    style T2 fill:#1a2030,stroke:#4cc9f0
    style L2 fill:#1a2030,stroke:#b794f4

Interceptor は RxJS の Observable を使うため、不要な演算子を入れるとオーバーヘッドになる。

// ❌ 悪い:何もしない map() を入れる
return next.handle().pipe(
  map(data => data), // ← 無意味
);

// ✅ 良い:必要な処理のみ
return next.handle().pipe(
  tap(() => /* log */),
);

// ✅ 良い:レスポンス整形
return next.handle().pipe(
  map(data => ({ data, timestamp: Date.now() })),
);

ExecutionContext / ArgumentsHost ─ コンテキストを取得する API

Guard / Interceptor / Filter には ExecutionContext が渡される。これは ArgumentsHost を継承していて、HTTP / RPC / WebSocket / GraphQL のコンテキストを抽象的に扱うための API だ。

canActivate(context: ExecutionContext): boolean {
  // 1. 種別の判定
  const type = context.getType(); // 'http' | 'rpc' | 'ws' | 'graphql'

  // 2. HTTP コンテキスト
  if (type === 'http') {
    const req = context.switchToHttp().getRequest<Request>();
    const res = context.switchToHttp().getResponse<Response>();
  }

  // 3. RPC(Microservice)コンテキスト
  if (type === 'rpc') {
    const data = context.switchToRpc().getData();
    const ctx = context.switchToRpc().getContext();
  }

  // 4. WebSocket コンテキスト
  if (type === 'ws') {
    const data = context.switchToWs().getData();
    const client = context.switchToWs().getClient();
  }

  // 5. ExecutionContext 固有:handler / class への参照
  const handler = context.getHandler();   // 次に呼ばれるメソッド
  const cls = context.getClass();         // controller クラス

  // 6. メタデータ取得(Reflector 経由)
  const roles = this.reflector.get(Roles, handler);

  return /* 判定 */;
}

switchToHttp() 等は薄いラッパで、コンテキスト依存のオブジェクトを返すだけ。生の getArgs() / getArgByIndex() もあるが、HTTP / RPC / WS で意味が変わるので使うべきでない。

アンチパターン:Interceptor で DB アクセス

「全リクエストにユーザー情報を付ける」ために Interceptor で DB を引く ─ これは典型的なアンチパターン。

// ❌ 悪い:Interceptor で毎リクエスト DB アクセス
@Injectable()
export class UserContextInterceptor implements NestInterceptor {
  constructor(private userService: UserService) {}

  async intercept(context: ExecutionContext, next: CallHandler) {
    const req = context.switchToHttp().getRequest();
    req.user = await this.userService.findById(req.userId); // ← 全 endpoint で DB 引く
    return next.handle();
  }
}

問題:

  • すべてのリクエストで DB が走る(unauth でもheath check でも)
  • N+1 を生むプロデューサーになる
  • Interceptor の責務(横断関心事)の範囲を超えている
// ✅ 良い:Guard で認証、Service が必要に応じて引く
@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const req = context.switchToHttp().getRequest();
    const token = extractToken(req);
    if (!token) return false;
    req.userId = verifyToken(token); // ← トークン検証だけ、DB 引かない
    return true;
  }
}

// 必要な Service だけが、必要なときに DB を引く
@Injectable()
export class CatsService {
  constructor(@Inject(REQUEST) private req: Request, private users: UserRepository) {}
  async findOwner() {
    return await this.users.findById(this.req.userId); // ← 必要なときだけ
  }
}

アンチパターン:Service 内 try-catch 多用

// ❌ 悪い:try-catch で覆い、独自のエラー型に変換
async create(dto: CreateCatDto) {
  try {
    return await this.repo.save(dto);
  } catch (e) {
    if (e.code === '23505') {
      throw new ConflictException('Already exists');
    }
    throw new InternalServerErrorException(e.message);
  }
}

// ✅ 良い:throw に統一、Filter で一括整形
async create(dto: CreateCatDto) {
  return await this.repo.save(dto);
}

// グローバル Filter で DB エラーを HTTP に変換
@Catch(QueryFailedError)
export class DbExceptionFilter implements ExceptionFilter {
  catch(ex: QueryFailedError, host: ArgumentsHost) {
    const status = ex.driverError?.code === '23505' ? 409 : 500;
    /* レスポンス整形 */
  }
}

スタックトレースが残る / エラー処理が一箇所に集約される / テストが書きやすい ─ これが NestJS の意図する設計だ。

✅ 良い実装の指針

Middleware:
  ✅ ログ、CORS、ヘッダ操作
  ❌ ビジネスロジック

Guard:
  ✅ JWT 検証、Role チェック(DB 引かずに済むもの)
  ❌ DB アクセス、外部 API

Interceptor (before):
  ✅ タイマー開始、リクエスト ID 生成、ログ
  ❌ DB アクセス

Pipe:
  ✅ class-validator、ParseIntPipe、変換
  ❌ 副作用、外部 API

Handler:
  ✅ Service 呼び出し、結果返却
  ❌ HTTP 詳細を意識する処理

Interceptor (after):
  ✅ レスポンス整形、tap 系のログ・キャッシュ
  ❌ DB アクセス

Exception Filter:
  ✅ エラーを HTTP レスポンスに整形
  ❌ ビジネスロジック、リトライ

本章の要点

#要点
1リクエストライフサイクルは Middleware → Guard → Interceptor (before) → Pipe → Handler → Interceptor (after) → Exception Filter の順
2Interceptor は handler を「囲む」構造で、next.handle() の前後が before / after
3useGlobalGuards(new Guard()) は DI が効かない。APP_GUARD token を使うAPP_PIPE / APP_INTERCEPTOR / APP_FILTER も同様)
4Pipe は引数ごとに last → first で適用
5Interceptor は first-in-last-out。RxJS の不要演算子はオーバーヘッド
6ExecutionContext は HTTP / RPC / WS / GraphQL を抽象化、switchTo*() でコンテキスト取得
7各層の責務違反(Interceptor で DB / Pipe で外部API / Service 内 try-catch)はアンチパターンの代表
8エラーは throw に統一、Filter で一括処理。NestJS の哲学

効いている根本原理

本章は 原理3(リクエスト境界の「層」を正しく使い分ける) そのものだった。「どこで何をすべきか」を間違えると、第9章のアンチパターンの大半が生まれる

ここまでで第1章(DI)・第2章(Decorator)・第3章(リクエストライフサイクル)と、NestJS の実行時の動きを分解した。次の第4章では、これらを束ねるModule システムと、v11 で起きた3つの大きな変更(Module 解決アルゴリズム / ライフサイクルフック順序 / Express v5)を詳しく見ていく。