NestJS-Interview: Guards, Interceptors und modulare Architektur

Häufige NestJS-Interviewfragen zu Guards, Interceptors und modularer Architektur mit konkreten TypeScript-Codebeispielen und technischen Erklärungen.

NestJS Guards, Interceptors und modulare Architektur für technische Interviews

Technische NestJS-Interviews konzentrieren sich konsequent auf drei Säulen des Frameworks: Guards, Interceptors und modulare Architektur. Diese Mechanismen bilden das Rückgrat jeder gut strukturierten NestJS-Anwendung, und Recruiter nutzen sie, um die tatsächliche Beherrschung des Frameworks zu bewerten.

Was Interviewer bewerten

Ein Guard kontrolliert den Zugriff (wer reinkommt), ein Interceptor transformiert Daten (was rein- und rausgeht), und die modulare Architektur organisiert alles. Die Beherrschung aller drei zeigt ein tiefes NestJS-Verständnis jenseits einfacher CRUD-Operationen.

Wie NestJS-Guards funktionieren und typische Interview-Szenarien

Guards implementieren das Interface CanActivate und entscheiden, ob ein Request den Handler erreicht. Im Gegensatz zu Middleware greifen Guards auf den Ausführungskontext von NestJS (ExecutionContext) zu, wodurch Autorisierungsentscheidungen anhand von Handler-Metadaten möglich werden.

Klassische Frage: "Implementieren Sie einen Guard, der die Benutzerrollen prüft."

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

Der Reflector liest die Metadaten, die von benutzerdefinierten Decorators angehängt wurden. Der zugehörige @Roles()-Decorator sieht so aus:

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

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

Eine starke Interview-Antwort geht über den Code hinaus. Die Ausführungsreihenfolge ist entscheidend: Middleware → Guards → Interceptors (vorher) → Pipes → Handler → Interceptors (nachher) → Exception Filters. Diese Reihenfolge erscheint in praktisch jedem NestJS-Interview.

NestJS-Interceptors: Requests und Responses transformieren

Interceptors implementieren NestInterceptor und nutzen RxJS, um den Datenstrom vor und nach dem Handler zu manipulieren. Ihre Stärke liegt im Zugriff auf CallHandler, der die Ausführungspipeline repräsentiert.

Häufige Frage: "Erstellen Sie einen Logging-Interceptor, der die Ausführungszeit misst."

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

Der RxJS-Operator tap beobachtet den Stream, ohne ihn zu verändern. Um die Response zu transformieren, ist map der richtige 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,
      })),
    );
  }
}

Dieses standardisierte Response-Pattern punktet im Interview: Es zeigt die Fähigkeit, über API-Design und Response-Konsistenz nachzudenken.

Guard vs Interceptor vs Middleware

Middleware: rohe HTTP-Verarbeitung (wie in Express). Guard: boolesche Zugriffsentscheidung. Interceptor: Transformation des Request/Response-Streams mit RxJS. Diese drei Konzepte im Interview zu verwechseln ist eine sofortige Red Flag.

Modulare NestJS-Architektur: skalierbare Anwendungen bauen

Die modulare Architektur trennt im Interview Junior- von Senior-Entwicklern. NestJS erzwingt eine modulbasierte Organisation, aber die echten Fragen zielen auf architektonische Entscheidungen: wann ein Modul erstellt werden sollte und wie modulübergreifende Abhängigkeiten verwaltet werden.

Fortgeschrittene Frage: "Wie würden Sie eine NestJS-E-Commerce-Anwendung mit sauber entkoppelten Modulen strukturieren?"

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

Die klassische Interview-Falle: zirkuläre Abhängigkeiten. Wenn OrdersModule PaymentsModule importiert und PaymentsModule OrdersModule importiert, schlägt NestJS beim Start fehl. Der Notausgang nutzt forwardRef:

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

Die stärkste Interview-Antwort erklärt, dass forwardRef ein Workaround und keine Lösung ist. Die korrekte Behebung besteht darin, ein gemeinsames Modul (SharedOrderPaymentModule) auszulagern oder einen Event Emitter einzusetzen, um die Kommunikation zu entkoppeln.

Bereit für deine Node.js / NestJS-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Dynamic Modules und fortgeschrittene Konfiguration

Dynamic Modules erstellen konfigurierbare, projektübergreifend wiederverwendbare Module. Dieses Pattern erscheint in Senior-Interviews.

Frage: "Erstellen Sie ein konfigurierbares Cache-Modul mit dynamischen Optionen."

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

Der zugehörige Service injiziert die Optionen über @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;
  }
}

Das Pattern forRoot / forRootAsync ist ein NestJS-Klassiker. forRootAsync lädt die Konfiguration aus einem injizierbaren ConfigService, was die empfohlene Produktionspraxis darstellt.

Interview-Falle: @Global()

Ein Modul als @Global() zu kennzeichnen wirkt bequem, aber übermäßige Nutzung erzeugt unsichtbare Kopplung. Im Interview zu erwähnen, dass @Global() für querschnittliche Services (Config, Cache, Logger) reserviert sein sollte, zeigt architektonische Reife.

Custom Decorators und Guard-Komposition

Custom Decorators kombinieren mehrere Guards und Metadaten in einer einzigen Annotation. Dieses Thema kommt für Mid-Senior-Positionen vor.

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

Verwendung in einem 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);
  }
}

Das applyDecorators-Pattern vereinfacht den Controller-Code und zentralisiert die Autorisierungslogik. Diesen Ansatz proaktiv im Interview vorzuschlagen ist ein starkes Signal.

Fazit

  • Guards nutzen CanActivate und den Reflector für Zugriffsentscheidungen anhand von Handler-Metadaten
  • Interceptors setzen RxJS (tap, map) ein, um den Stream vor und nach dem Handler zu transformieren
  • Die Ausführungsreihenfolge Middleware → Guards → Interceptors → Pipes → Handler ist eine nahezu universelle Frage
  • Zirkuläre Abhängigkeiten sollten durch architektonisches Refactoring gelöst werden, nicht durch forwardRef
  • Dynamic Modules (forRoot/forRootAsync) zeigen die Fähigkeit, wiederverwendbare Komponenten zu bauen
  • applyDecorators kombiniert mehrere Guards in einem einzigen, lesbaren Decorator
  • Üben Sie diese Fragen mit echtem Code zu NestJS-Modulen und Interceptors

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Tags

#nestjs
#guards
#interceptors
#architecture
#interview

Teilen

Verwandte Artikel