NestJS面接:Guards、Interceptors、モジュラーアーキテクチャ

Guards、Interceptors、モジュラーアーキテクチャに関するNestJS技術面接の頻出質問を、具体的なTypeScriptコード例と技術解説とともに紹介します。

技術面接のためのNestJS Guards、Interceptors、モジュラーアーキテクチャ

NestJSの技術面接では、フレームワークの3つの柱に一貫して焦点が当てられます。Guards、Interceptors、そしてモジュラーアーキテクチャです。これらのメカニズムは、よく構造化されたNestJSアプリケーションの背骨を形成し、採用担当者はフレームワークへの実際の習熟度を評価するために用います。

面接官が評価するポイント

Guardはアクセスを制御し(誰が入れるか)、Interceptorはデータを変換し(何が出入りするか)、モジュラーアーキテクチャはすべてを整理します。これら3つを使いこなすことは、基本的なCRUD操作を超えたNestJSへの深い理解を示します。

NestJSのGuardsの仕組みと面接の典型的なシナリオ

GuardsはCanActivateインターフェースを実装し、リクエストがハンドラーに到達するかを判定します。ミドルウェアと異なり、GuardsはNestJSの実行コンテキスト(ExecutionContext)にアクセスでき、ハンドラーのメタデータに基づいた認可判断を可能にします。

定番の質問: 「ユーザーのロールを検証するGuardを実装してください。」

roles.guard.tstypescript
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

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

  canActivate(context: ExecutionContext): boolean {
    // Retrieve roles defined via the @Roles() decorator
    const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
      context.getHandler(),
      context.getClass(),
    ]);

    // No roles required means the route is public
    if (!requiredRoles) return true;

    // Extract user from the HTTP request
    const { user } = context.switchToHttp().getRequest();

    // Check if the user holds at least one required role
    return requiredRoles.some((role) => user.roles?.includes(role));
  }
}

Reflectorは、カスタムデコレータによって付与されたメタデータを読み取ります。対応する@Roles()デコレータは次のような形になります。

roles.decorator.tstypescript
import { SetMetadata } from '@nestjs/common';

// Creates a decorator that attaches roles as metadata
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

面接で強い回答はコードを超えた説明を含みます。実行順序が重要です。Middleware → Guards → Interceptors(前)→ Pipes → Handler → Interceptors(後)→ Exception Filtersという流れは、ほぼすべてのNestJS面接で問われます。

NestJSのInterceptors:リクエストとレスポンスの変換

InterceptorsはNestInterceptorを実装し、RxJSを使ってハンドラーの前後でデータストリームを操作します。その力は、実行パイプラインを表すCallHandlerへのアクセスから生まれます。

よくある質問: 「実行時間を計測するロギングInterceptorを作成してください。」

logging.interceptor.tstypescript
import { 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;
    const now = Date.now();

    // next.handle() invokes the route handler
    return next.handle().pipe(
      tap(() => {
        // Runs AFTER the handler responds
        const duration = Date.now() - now;
        this.logger.log(`${method} ${url}${duration}ms`);
      }),
    );
  }
}

RxJSのtapオペレーターは、ストリームを変更せずに観測します。レスポンスを変換するには、適切なオペレーターはmapです。

transform.interceptor.tstypescript
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

// Generic interface for a standardized API response
interface ApiResponse<T> {
  data: T;
  timestamp: string;
  path: string;
}

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponse<T>> {
    const request = context.switchToHttp().getRequest();

    return next.handle().pipe(
      map((data) => ({
        data,
        timestamp: new Date().toISOString(),
        path: request.url,
      })),
    );
  }
}

この標準化されたレスポンスパターンは面接で高評価につながります。API設計とレスポンスの一貫性を考える力を示せるからです。

Guard vs Interceptor vs Middleware

Middleware: 生のHTTP処理(Expressに似ています)。Guard: アクセスに関する真偽の判断。Interceptor: RxJSによるリクエスト/レスポンスストリームの変換。面接でこれら3つを混同するのは即座のレッドフラッグになります。

NestJSのモジュラーアーキテクチャ:スケーラブルなアプリケーションを構築する

モジュラーアーキテクチャは、面接でジュニアとシニアの開発者を分けます。NestJSはモジュールベースの構成を強制しますが、本質的な質問はアーキテクチャ判断を狙います。いつモジュールを作成するか、モジュール間の依存関係をどう管理するかです。

上級の質問: 「適切に疎結合化されたモジュールでNestJSのEコマースアプリケーションをどう構造化しますか。」

orders/orders.module.tstypescript
import { Module } from '@nestjs/common';
import { OrdersService } from './orders.service';
import { OrdersController } from './orders.controller';
import { PaymentsModule } from '../payments/payments.module';
import { ProductsModule } from '../products/products.module';

@Module({
  // Import modules that OrdersModule depends on
  imports: [PaymentsModule, ProductsModule],
  controllers: [OrdersController],
  providers: [OrdersService],
  // Expose OrdersService to modules that import OrdersModule
  exports: [OrdersService],
})
export class OrdersModule {}

面接の典型的な落とし穴は循環依存です。OrdersModulePaymentsModuleをインポートし、PaymentsModuleOrdersModuleをインポートすると、NestJSは起動時に失敗します。緊急脱出手段はforwardRefの使用です。

typescript
// Resolving circular dependencies
@Module({
  imports: [forwardRef(() => PaymentsModule)],
})
export class OrdersModule {}

最も強い回答は、forwardRefは対症療法であって解決策ではないと説明します。正しい修正は、共有モジュール(SharedOrderPaymentModule)を抽出するリファクタリングか、Event Emitterを使って通信を疎結合化することです。

Node.js / NestJSの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

Dynamic Modulesと高度な設定

Dynamic Modulesは、プロジェクト間で再利用可能な設定可能なモジュールを作成します。このパターンはシニアレベルの面接で登場します。

質問: 「動的オプションを持つ設定可能なキャッシュモジュールを作成してください。」

cache/cache.module.tstypescript
import { Module, DynamicModule, Global } from '@nestjs/common';
import { CacheService } from './cache.service';

// Module configuration interface
export interface CacheModuleOptions {
  ttl: number;        // Time-to-live in seconds
  maxItems: number;   // Maximum number of entries
  prefix: string;     // Key prefix
}

@Global() // Available everywhere without explicit import
@Module({})
export class CacheModule {
  static forRoot(options: CacheModuleOptions): DynamicModule {
    return {
      module: CacheModule,
      providers: [
        {
          // Inject options via a custom token
          provide: 'CACHE_OPTIONS',
          useValue: options,
        },
        CacheService,
      ],
      exports: [CacheService],
    };
  }
}

対応するサービスは@Inject()を介してオプションを注入します。

cache/cache.service.tstypescript
import { Injectable, Inject } from '@nestjs/common';
import { CacheModuleOptions } from './cache.module';

@Injectable()
export class CacheService {
  private store = new Map<string, { value: any; expiry: number }>();

  constructor(@Inject('CACHE_OPTIONS') private options: CacheModuleOptions) {}

  set(key: string, value: any): void {
    const prefixedKey = `${this.options.prefix}:${key}`;
    this.store.set(prefixedKey, {
      value,
      expiry: Date.now() + this.options.ttl * 1000,
    });
  }

  get<T>(key: string): T | null {
    const prefixedKey = `${this.options.prefix}:${key}`;
    const entry = this.store.get(prefixedKey);
    // Check expiration before returning
    if (!entry || entry.expiry < Date.now()) return null;
    return entry.value as T;
  }
}

forRoot / forRootAsyncパターンはNestJSの定番です。forRootAsyncは注入可能なConfigServiceから設定を読み込み、本番環境で推奨されるプラクティスとなっています。

面接の落とし穴:@Global()

モジュールを@Global()としてマークするのは便利に見えますが、乱用すると見えない結合が生まれます。@Global()は横断的なサービス(config、cache、logger)に限定すべきだと面接で言及することは、アーキテクチャ的成熟度を示します。

カスタムデコレータとGuardsの合成

カスタムデコレータは、複数のGuardsとメタデータを単一のアノテーションに結合します。このトピックはミッドシニアのポジションで取り上げられます。

auth.decorator.tstypescript
import { applyDecorators, UseGuards, SetMetadata } from '@nestjs/common';
import { JwtAuthGuard } from './jwt-auth.guard';
import { RolesGuard } from './roles.guard';

// Combines JWT authentication + role verification
export function Auth(...roles: string[]) {
  return applyDecorators(
    SetMetadata('roles', roles),
    UseGuards(JwtAuthGuard, RolesGuard),
  );
}

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

orders/orders.controller.tstypescript
import { Controller, Get, Post, Body } from '@nestjs/common';
import { Auth } from '../auth/auth.decorator';
import { OrdersService } from './orders.service';

@Controller('orders')
export class OrdersController {
  constructor(private ordersService: OrdersService) {}

  @Get()
  @Auth('admin', 'manager') // Single decorator replaces UseGuards + Roles
  findAll() {
    return this.ordersService.findAll();
  }

  @Post()
  @Auth('admin')
  create(@Body() dto: CreateOrderDto) {
    return this.ordersService.create(dto);
  }
}

applyDecoratorsパターンはコントローラーのコードをシンプルにし、認可ロジックを一元化します。面接でこのアプローチを能動的に提案することは強いシグナルになります。

まとめ

  • GuardsはCanActivateReflectorを使い、ハンドラーのメタデータに基づくアクセス判断を行います
  • InterceptorsはRxJS(tapmap)を活用し、ハンドラーの前後でストリームを変換します
  • 実行順序Middleware → Guards → Interceptors → Pipes → Handlerはほぼ普遍的な質問です
  • 循環依存はforwardRefではなく、アーキテクチャのリファクタリングで解決すべきです
  • Dynamic Modules(forRoot/forRootAsync)は再利用可能なコンポーネントを構築する力を示します
  • applyDecoratorsは複数のGuardsを読みやすい一つのデコレータに合成します
  • これらの質問を実コードで練習するなら、NestJSモジュールInterceptorsをご覧ください

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

タグ

#nestjs
#guards
#interceptors
#architecture
#interview

共有

関連記事