NestJS en entretien : Guards, Interceptors et Architecture modulaire

Les questions d'entretien NestJS sur les Guards, Interceptors et l'architecture modulaire, avec des exemples de code concrets et des explications techniques.

NestJS Guards Interceptors et architecture modulaire pour entretien technique

Les entretiens techniques NestJS couvrent systématiquement trois piliers du framework : les Guards, les Interceptors et l'architecture modulaire. Ces mécanismes constituent le socle de toute application NestJS bien structurée, et les recruteurs s'en servent pour évaluer la maîtrise réelle du framework.

Ce que les recruteurs évaluent

Un Guard contrôle l'accès (qui peut entrer), un Interceptor transforme les données (ce qui entre et sort), et l'architecture modulaire organise le tout. Maîtriser ces trois concepts démontre une compréhension profonde de NestJS au-delà du simple CRUD.

Fonctionnement des Guards NestJS et cas d'usage en entretien

Les Guards implémentent l'interface CanActivate et déterminent si une requête peut atteindre le handler. Contrairement aux middleware, ils ont accès au contexte d'exécution NestJS (ExecutionContext), ce qui permet des décisions d'autorisation basées sur les métadonnées du handler.

Question classique : "Implémenter un Guard qui vérifie les rôles utilisateur."

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 {
    // Récupère les rôles définis via le décorateur @Roles()
    const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
      context.getHandler(),
      context.getClass(),
    ]);

    // Si aucun rôle requis, la route est accessible
    if (!requiredRoles) return true;

    // Extrait l'utilisateur depuis la requête HTTP
    const { user } = context.switchToHttp().getRequest();

    // Vérifie que l'utilisateur possède au moins un rôle requis
    return requiredRoles.some((role) => user.roles?.includes(role));
  }
}

Le Reflector est la clé : il lit les métadonnées attachées par les décorateurs personnalisés. Le décorateur @Roles() associé ressemble à ceci :

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

// Crée un décorateur qui attache les rôles comme métadonnée
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

En entretien, la réponse attendue ne s'arrête pas au code. Il faut expliquer l'ordre d'exécution : Middleware → Guards → Interceptors (before) → Pipes → Handler → Interceptors (after) → Exception Filters. Cette séquence revient dans pratiquement tous les entretiens NestJS.

Interceptors NestJS : transformer les requêtes et réponses

Les Interceptors implémentent NestInterceptor et utilisent RxJS pour manipuler le flux de données avant et après le handler. Leur puissance réside dans l'accès au CallHandler, qui représente le pipeline d'exécution.

Question fréquente : "Créer un Interceptor de logging qui mesure le temps d'exécution."

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() appelle le handler de la route
    return next.handle().pipe(
      tap(() => {
        // Exécuté APRÈS la réponse du handler
        const duration = Date.now() - now;
        this.logger.log(`${method} ${url}${duration}ms`);
      }),
    );
  }
}

L'opérateur tap de RxJS observe le flux sans le modifier. Pour transformer la réponse, map est l'opérateur adapté :

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

// Interface générique pour une réponse API standardisée
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,
      })),
    );
  }
}

Ce pattern de réponse standardisée est très apprécié en entretien : il montre la capacité à penser "API design" et cohérence des réponses.

Guard vs Interceptor vs Middleware

Middleware : traitement HTTP brut (comme Express). Guard : décision booléenne d'accès. Interceptor : transformation du flux requête/réponse avec RxJS. En entretien, confondre ces trois concepts est un signal négatif immédiat.

Architecture modulaire NestJS : organiser une application scalable

L'architecture modulaire est le sujet qui distingue les développeurs juniors des seniors en entretien. NestJS impose une organisation en modules, mais la vraie question porte sur les décisions architecturales : quand créer un module ? Comment gérer les dépendances entre modules ?

Question avancée : "Comment structurer une application e-commerce NestJS avec des modules bien découplés ?"

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({
  // Importe les modules dont OrdersModule dépend
  imports: [PaymentsModule, ProductsModule],
  controllers: [OrdersController],
  providers: [OrdersService],
  // Expose OrdersService pour les modules qui importent OrdersModule
  exports: [OrdersService],
})
export class OrdersModule {}

Le piège classique en entretien : les dépendances circulaires. Si OrdersModule importe PaymentsModule et PaymentsModule importe OrdersModule, NestJS échoue au démarrage. La solution utilise forwardRef :

typescript
// Résolution des dépendances circulaires
@Module({
  imports: [forwardRef(() => PaymentsModule)],
})
export class OrdersModule {}

Mais la meilleure réponse en entretien consiste à expliquer que forwardRef est un contournement : la vraie solution est de refactorer pour extraire un module partagé (SharedOrderPaymentModule) ou d'utiliser un Event Emitter pour découpler les communications.

Prêt à réussir tes entretiens Node.js / NestJS ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Dynamic Modules et configuration avancée

Les Dynamic Modules permettent de créer des modules configurables, réutilisables entre projets. Ce pattern apparaît dans les entretiens pour les postes senior.

Question : "Créer un module de cache configurable avec des options dynamiques."

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

// Interface de configuration du module
export interface CacheModuleOptions {
  ttl: number;        // Durée de vie en secondes
  maxItems: number;   // Nombre max d'entrées
  prefix: string;     // Préfixe des clés
}

@Global() // Disponible partout sans import explicite
@Module({})
export class CacheModule {
  static forRoot(options: CacheModuleOptions): DynamicModule {
    return {
      module: CacheModule,
      providers: [
        {
          // Injecte les options via un token personnalisé
          provide: 'CACHE_OPTIONS',
          useValue: options,
        },
        CacheService,
      ],
      exports: [CacheService],
    };
  }
}

Le service correspondant injecte les options via @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);
    // Vérifie l'expiration avant de retourner
    if (!entry || entry.expiry < Date.now()) return null;
    return entry.value as T;
  }
}

Le pattern forRoot / forRootAsync est un classique NestJS. forRootAsync permet de charger la configuration depuis un ConfigService injectable, ce qui est la pratique recommandée en production.

Piège d'entretien : @Global()

Marquer un module @Global() semble pratique, mais en abuser crée un couplage invisible. En entretien, mentionner que @Global() doit être réservé aux services transversaux (config, cache, logger) démontre une maturité architecturale.

Custom Decorators et composition de Guards

Les décorateurs personnalisés combinent plusieurs Guards et métadonnées en une seule annotation. Ce sujet revient pour les postes mid-senior.

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

// Combine authentification JWT + vérification des rôles
export function Auth(...roles: string[]) {
  return applyDecorators(
    SetMetadata('roles', roles),
    UseGuards(JwtAuthGuard, RolesGuard),
  );
}

Utilisation dans un contrôleur :

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') // Un seul décorateur remplace UseGuards + Roles
  findAll() {
    return this.ordersService.findAll();
  }

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

Ce pattern applyDecorators simplifie le code des contrôleurs et centralise la logique d'autorisation. En entretien, proposer spontanément cette approche est un signal fort.

Conclusion

  • Les Guards utilisent CanActivate et le Reflector pour des décisions d'accès basées sur les métadonnées des handlers
  • Les Interceptors exploitent RxJS (tap, map) pour transformer le flux avant et après le handler
  • L'ordre d'exécution Middleware → Guards → Interceptors → Pipes → Handler est une question quasi-systématique
  • Les dépendances circulaires se résolvent par refactoring architectural, pas par forwardRef
  • Les Dynamic Modules (forRoot/forRootAsync) démontrent la capacité à créer des composants réutilisables
  • applyDecorators compose plusieurs Guards en un décorateur unique et lisible
  • Pratiquer ces questions avec du code réel sur les modules NestJS et les Interceptors renforce la préparation

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#nestjs
#guards
#interceptors
#architecture
#entretien

Partager

Articles similaires