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.

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.
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."
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 :
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."
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é :
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.
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 ?"
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 :
// 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."
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() :
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.
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.
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 :
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
CanActivateet leReflectorpour 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 applyDecoratorscompose 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
Partager
Articles similaires

NestJS + Prisma : La stack moderne pour le backend Node.js
Guide complet pour créer une API backend moderne avec NestJS et Prisma. Configuration, modèles, services, transactions et bonnes pratiques expliquées.

NestJS : Créer une API REST complète
Guide complet pour créer une API REST professionnelle avec NestJS. Controllers, Services, Modules, validation avec class-validator et gestion des erreurs expliqués.

Performance Node.js : Event Loop, Clustering et Optimisation en 2026
Optimisation des performances Node.js par la gestion de l'event loop, les stratégies de clustering et les worker threads. Patterns pratiques pour des applications Node.js haute performance en 2026.