Співбесіда NestJS: Guards, Interceptors і модульна архітектура

Часті питання технічних співбесід з NestJS щодо Guards, Interceptors і модульної архітектури з конкретними прикладами коду TypeScript і технічними поясненнями.

NestJS Guards, Interceptors і модульна архітектура для технічних співбесід

Технічні співбесіди з NestJS послідовно зосереджуються на трьох стовпах фреймворка: Guards, Interceptors і модульній архітектурі. Ці механізми утворюють кістяк будь-якого добре структурованого NestJS-додатка, і рекрутери використовують їх для оцінки реального володіння фреймворком.

Що оцінюють співбесідники

Guard контролює доступ (хто заходить), Interceptor трансформує дані (що входить і виходить), а модульна архітектура все організовує. Опанування всіх трьох демонструє глибоке розуміння NestJS поза базовими CRUD-операціями.

Як працюють Guards у NestJS і типові сценарії співбесід

Guards реалізують інтерфейс CanActivate і визначають, чи досягне запит обробника. На відміну від middleware, Guards мають доступ до контексту виконання NestJS (ExecutionContext), що дозволяє ухвалювати рішення про авторизацію на основі метаданих обробника.

Класичне питання: "Реалізуйте Guard, який перевіряє ролі користувача."

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 зчитує метадані, прикріплені користувацькими декораторами. Відповідний декоратор @Roles() має такий вигляд:

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

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

Сильна відповідь на співбесіді виходить за межі коду. Порядок виконання має значення: Middleware → Guards → Interceptors (до) → Pipes → Handler → Interceptors (після) → Exception Filters. Ця послідовність трапляється практично на кожній співбесіді з NestJS.

Interceptors NestJS: трансформація запитів і відповідей

Interceptors реалізують NestInterceptor і використовують RxJS для маніпулювання потоком даних до й після обробника. Їхня сила — у доступі до CallHandler, який представляє конвеєр виконання.

Часте питання: "Створіть Interceptor для логування, який вимірює час виконання."

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

Оператор RxJS tap спостерігає за потоком, не змінюючи його. Для трансформації відповіді правильним оператором є 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,
      })),
    );
  }
}

Цей патерн стандартизованої відповіді набирає очки на співбесідах: він демонструє вміння думати про дизайн API і узгодженість відповідей.

Guard vs Interceptor vs Middleware

Middleware: сира HTTP-обробка (як Express). Guard: булеве рішення про доступ. Interceptor: трансформація потоку запит/відповідь за допомогою RxJS. Плутання цих трьох понять на співбесіді — миттєвий червоний прапорець.

Модульна архітектура NestJS: побудова масштабованих застосунків

Модульна архітектура відокремлює на співбесідах джуніорів від сеньйорів. NestJS вимагає організації на основі модулів, але справжні питання спрямовані на архітектурні рішення: коли створювати модуль і як керувати залежностями між модулями.

Просунуте питання: "Як би ви структурували NestJS-застосунок електронної комерції з належно розв'язаними модулями?"

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

Класична пастка співбесіди: циклічні залежності. Якщо OrdersModule імпортує PaymentsModule, а PaymentsModule імпортує OrdersModule, NestJS падає під час старту. Аварійний вихід використовує forwardRef:

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

Найсильніша відповідь пояснює, що forwardRef — це обхідний шлях, а не виправлення. Належне виправлення полягає в рефакторингу для виділення спільного модуля (SharedOrderPaymentModule) або у використанні Event Emitter для розв'язання комунікацій.

Готовий до співбесід з Node.js / NestJS?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Dynamic Modules і просунута конфігурація

Dynamic Modules створюють налаштовувані модулі, придатні для повторного використання між проєктами. Цей патерн з'являється на сеньйорських співбесідах.

Питання: "Створіть налаштовуваний модуль кешу з динамічними опціями."

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

Відповідний сервіс впроваджує опції через @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;
  }
}

Патерн forRoot / forRootAsync — класика NestJS. forRootAsync завантажує конфігурацію з ін'єктовного ConfigService, що є рекомендованою практикою на продакшені.

Пастка співбесіди: @Global()

Позначати модуль як @Global() здається зручним, але зловживання цим створює невидимі зв'язки. Згадати на співбесіді, що @Global() має бути зарезервованим для крізних сервісів (config, cache, logger), демонструє архітектурну зрілість.

Користувацькі декоратори та композиція Guards

Користувацькі декоратори об'єднують кілька Guards і метадані в одну анотацію. Ця тема виникає на позиціях 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),
  );
}

Використання у контролері:

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

Патерн applyDecorators спрощує код контролера й централізує логіку авторизації. Проактивно запропонувати цей підхід на співбесіді — сильний сигнал.

Висновок

  • Guards використовують CanActivate і Reflector для рішень про доступ на основі метаданих обробника
  • Interceptors використовують RxJS (tap, map) для трансформації потоку до й після обробника
  • Порядок виконання Middleware → Guards → Interceptors → Pipes → Handler — майже універсальне питання
  • Циклічні залежності слід розв'язувати через архітектурний рефакторинг, а не через forwardRef
  • Dynamic Modules (forRoot/forRootAsync) демонструють здатність будувати багаторазові компоненти
  • applyDecorators поєднує кілька Guards в один читабельний декоратор
  • Практикуйте ці питання на реальному коді у модулях NestJS та Interceptors

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Теги

#nestjs
#guards
#interceptors
#architecture
#interview

Поділитися

Пов'язані статті