NestJS-sollicitatiegesprek: Guards, Interceptors en modulaire architectuur

Veelgestelde vragen in technische NestJS-sollicitatiegesprekken over Guards, Interceptors en modulaire architectuur, met concrete TypeScript-codevoorbeelden en technische uitleg.

NestJS Guards, Interceptors en modulaire architectuur voor technische sollicitatiegesprekken

Technische NestJS-sollicitatiegesprekken richten zich consequent op drie pijlers van het framework: Guards, Interceptors en modulaire architectuur. Deze mechanismen vormen de ruggengraat van elke goed gestructureerde NestJS-applicatie, en recruiters gebruiken ze om de werkelijke beheersing van het framework te beoordelen.

Wat interviewers beoordelen

Een Guard regelt toegang (wie er binnenkomt), een Interceptor transformeert data (wat erin en eruit gaat), en de modulaire architectuur organiseert alles. Het beheersen van alle drie toont een diep begrip van NestJS dat verder gaat dan basale CRUD-operaties.

Hoe NestJS Guards werken en typische sollicitatiescenario's

Guards implementeren de CanActivate-interface en bepalen of een verzoek de handler bereikt. In tegenstelling tot middleware hebben Guards toegang tot de NestJS-uitvoeringscontext (ExecutionContext), waardoor autorisatiebeslissingen op basis van handler-metadata mogelijk zijn.

Klassieke vraag: "Implementeer een Guard die gebruikersrollen controleert."

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));
  }
}

De Reflector leest de metadata die door custom decorators is gekoppeld. De bijbehorende @Roles()-decorator ziet er zo uit:

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

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

Een sterk antwoord in een sollicitatiegesprek gaat verder dan de code. De uitvoeringsvolgorde is belangrijk: Middleware → Guards → Interceptors (vooraf) → Pipes → Handler → Interceptors (achteraf) → Exception Filters. Deze volgorde komt in vrijwel elk NestJS-gesprek terug.

NestJS Interceptors: requests en responses transformeren

Interceptors implementeren NestInterceptor en gebruiken RxJS om de datastroom voor en na de handler te manipuleren. Hun kracht komt uit de toegang tot CallHandler, dat de uitvoeringspijplijn vertegenwoordigt.

Veelvoorkomende vraag: "Maak een logging-Interceptor die de uitvoeringstijd meet."

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`);
      }),
    );
  }
}

De RxJS-operator tap observeert de stream zonder hem te wijzigen. Om de response te transformeren is map de juiste operator:

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,
      })),
    );
  }
}

Dit gestandaardiseerde response-patroon scoort goed in een gesprek: het toont het vermogen om na te denken over API-ontwerp en consistentie van responses.

Guard vs Interceptor vs Middleware

Middleware: ruwe HTTP-verwerking (zoals Express). Guard: booleaanse toegangsbeslissing. Interceptor: transformatie van de request/response-stream met RxJS. Deze drie concepten verwarren in een gesprek is een directe red flag.

Modulaire NestJS-architectuur: schaalbare applicaties bouwen

De modulaire architectuur scheidt junior van senior developers tijdens gesprekken. NestJS dwingt een module-gebaseerde organisatie af, maar de echte vragen richten zich op architectuurkeuzes: wanneer een module aan te maken en hoe afhankelijkheden tussen modules te beheren.

Gevorderde vraag: "Hoe zou je een NestJS e-commerce-applicatie structureren met goed ontkoppelde modules?"

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 {}

De klassieke valkuil: circulaire afhankelijkheden. Als OrdersModule PaymentsModule importeert en PaymentsModule OrdersModule importeert, faalt NestJS bij het opstarten. De noodoplossing gebruikt forwardRef:

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

Het sterkste antwoord legt uit dat forwardRef een tijdelijke oplossing is, geen echte fix. De juiste oplossing is refactoren door een gedeelde module (SharedOrderPaymentModule) te extraheren of een Event Emitter te gebruiken om communicatie te ontkoppelen.

Klaar om je Node.js / NestJS gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Dynamic Modules en geavanceerde configuratie

Dynamic Modules creëren configureerbare, projectoverstijgend herbruikbare modules. Dit patroon komt voor in senior-gesprekken.

Vraag: "Maak een configureerbare cachemodule met dynamische opties."

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],
    };
  }
}

De bijbehorende service injecteert de opties 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);
    // Check expiration before returning
    if (!entry || entry.expiry < Date.now()) return null;
    return entry.value as T;
  }
}

Het patroon forRoot / forRootAsync is een NestJS-klassieker. forRootAsync laadt de configuratie vanuit een injecteerbare ConfigService, wat de aanbevolen praktijk in productie is.

Valkuil: @Global()

Een module markeren als @Global() lijkt handig, maar overmatig gebruik creëert onzichtbare koppelingen. In een gesprek vermelden dat @Global() gereserveerd moet zijn voor cross-cutting services (config, cache, logger) toont architecturale volwassenheid.

Custom decorators en compositie van Guards

Custom decorators combineren meerdere Guards en metadata in één enkele annotatie. Dit onderwerp komt voor bij mid-senior-posities.

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),
  );
}

Gebruik in een controller:

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);
  }
}

Het applyDecorators-patroon vereenvoudigt de controllercode en centraliseert de autorisatielogica. Deze aanpak proactief voorstellen tijdens een gesprek is een sterk signaal.

Conclusie

  • Guards gebruiken CanActivate en de Reflector voor toegangsbeslissingen op basis van handler-metadata
  • Interceptors benutten RxJS (tap, map) om de stream voor en na de handler te transformeren
  • De uitvoeringsvolgorde Middleware → Guards → Interceptors → Pipes → Handler is een bijna universele vraag
  • Circulaire afhankelijkheden moeten via architectuurrefactoring worden opgelost, niet met forwardRef
  • Dynamic Modules (forRoot/forRootAsync) tonen het vermogen om herbruikbare componenten te bouwen
  • applyDecorators voegt meerdere Guards samen in één leesbare decorator
  • Oefen deze vragen met echte code over NestJS-modules en Interceptors

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Tags

#nestjs
#guards
#interceptors
#architecture
#interview

Delen

Gerelateerde artikelen