Wawancara NestJS: Guards, Interceptors, dan arsitektur modular

Pertanyaan umum dalam wawancara teknis NestJS tentang Guards, Interceptors, dan arsitektur modular dengan contoh kode TypeScript konkret dan penjelasan teknis.

Guards, Interceptors, dan arsitektur modular NestJS untuk wawancara teknis

Wawancara teknis NestJS secara konsisten berfokus pada tiga pilar framework: Guards, Interceptors, dan arsitektur modular. Mekanisme ini menjadi tulang punggung setiap aplikasi NestJS yang terstruktur dengan baik, dan perekrut menggunakannya untuk menilai penguasaan framework yang sesungguhnya.

Apa yang dinilai pewawancara

Guard mengontrol akses (siapa yang masuk), Interceptor mengubah data (apa yang masuk dan keluar), sementara arsitektur modular mengatur semuanya. Menguasai ketiganya menunjukkan pemahaman NestJS yang mendalam, melampaui operasi CRUD dasar.

Cara kerja Guards NestJS dan skenario wawancara umum

Guards mengimplementasikan antarmuka CanActivate dan menentukan apakah sebuah request mencapai handler. Berbeda dengan middleware, Guards mengakses execution context NestJS (ExecutionContext), yang memungkinkan keputusan otorisasi berdasarkan metadata handler.

Pertanyaan klasik: "Implementasikan Guard yang memeriksa peran pengguna."

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 membaca metadata yang dilampirkan oleh decorator kustom. Decorator @Roles() yang sesuai terlihat seperti ini:

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

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

Jawaban yang kuat dalam wawancara melampaui kode. Urutan eksekusi penting: Middleware → Guards → Interceptors (sebelum) → Pipes → Handler → Interceptors (sesudah) → Exception Filters. Urutan ini muncul di hampir setiap wawancara NestJS.

Interceptors NestJS: mengubah request dan response

Interceptors mengimplementasikan NestInterceptor dan menggunakan RxJS untuk memanipulasi aliran data sebelum dan sesudah handler. Kekuatannya berasal dari akses ke CallHandler, yang merepresentasikan pipeline eksekusi.

Pertanyaan yang sering muncul: "Buat sebuah Interceptor logging yang mengukur waktu eksekusi."

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

Operator RxJS tap mengamati stream tanpa mengubahnya. Untuk mengubah response, operator yang tepat adalah 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,
      })),
    );
  }
}

Pola response standar ini memberi nilai bagus dalam wawancara: menunjukkan kemampuan berpikir tentang desain API dan konsistensi response.

Guard vs Interceptor vs Middleware

Middleware: pemrosesan HTTP mentah (seperti Express). Guard: keputusan boolean untuk akses. Interceptor: transformasi aliran request/response dengan RxJS. Mencampurkan ketiga konsep ini dalam wawancara adalah red flag instan.

Arsitektur modular NestJS: membangun aplikasi yang skalabel

Arsitektur modular memisahkan developer junior dari senior dalam wawancara. NestJS memaksakan organisasi berbasis modul, tetapi pertanyaan sebenarnya menyasar keputusan arsitektur: kapan membuat modul dan bagaimana mengelola dependensi antar modul.

Pertanyaan tingkat lanjut: "Bagaimana Anda menstruktur aplikasi e-commerce NestJS dengan modul yang dipisahkan dengan baik?"

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

Jebakan klasik wawancara: dependensi siklik. Jika OrdersModule mengimpor PaymentsModule dan PaymentsModule mengimpor OrdersModule, NestJS gagal saat startup. Jalan keluar daruratnya menggunakan forwardRef:

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

Jawaban paling kuat menjelaskan bahwa forwardRef adalah jalan pintas, bukan solusi sebenarnya. Perbaikan yang tepat adalah merefaktor untuk mengekstrak modul bersama (SharedOrderPaymentModule) atau menggunakan Event Emitter untuk memisahkan komunikasi.

Siap menguasai wawancara Node.js / NestJS Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Dynamic Modules dan konfigurasi tingkat lanjut

Dynamic Modules menciptakan modul yang dapat dikonfigurasi dan digunakan ulang antar proyek. Pola ini muncul dalam wawancara level senior.

Pertanyaan: "Buat sebuah modul cache yang dapat dikonfigurasi dengan opsi dinamis."

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

Service yang sesuai menyuntikkan opsi melalui @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;
  }
}

Pola forRoot / forRootAsync adalah klasik NestJS. forRootAsync memuat konfigurasi dari ConfigService yang dapat di-inject, yang menjadi praktik produksi yang direkomendasikan.

Jebakan wawancara: @Global()

Menandai modul sebagai @Global() tampak praktis, tetapi penyalahgunaannya menciptakan kopling yang tidak terlihat. Menyebutkan dalam wawancara bahwa @Global() harus dicadangkan untuk service lintas-aplikasi (config, cache, logger) menunjukkan kematangan arsitektur.

Custom Decorators dan komposisi Guards

Custom decorators menggabungkan beberapa Guards dan metadata dalam satu anotasi tunggal. Topik ini muncul untuk posisi mid-senior.

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

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

Pola applyDecorators menyederhanakan kode controller dan memusatkan logika otorisasi. Mengusulkan pendekatan ini secara proaktif dalam wawancara adalah sinyal yang kuat.

Kesimpulan

  • Guards menggunakan CanActivate dan Reflector untuk keputusan akses berdasarkan metadata handler
  • Interceptors memanfaatkan RxJS (tap, map) untuk mengubah aliran sebelum dan sesudah handler
  • Urutan eksekusi Middleware → Guards → Interceptors → Pipes → Handler adalah pertanyaan yang hampir universal
  • Dependensi siklik harus diselesaikan melalui refactoring arsitektur, bukan dengan forwardRef
  • Dynamic Modules (forRoot/forRootAsync) menunjukkan kemampuan membangun komponen yang dapat digunakan kembali
  • applyDecorators menggabungkan beberapa Guards menjadi satu decorator yang mudah dibaca
  • Latihlah pertanyaan-pertanyaan ini dengan kode nyata pada modul NestJS dan Interceptors

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Tag

#nestjs
#guards
#interceptors
#architecture
#interview

Bagikan

Artikel terkait