สัมภาษณ์ NestJS: Guards, Interceptors และสถาปัตยกรรมแบบโมดูล

คำถามที่พบบ่อยในการสัมภาษณ์เทคนิค NestJS เกี่ยวกับ Guards, Interceptors และสถาปัตยกรรมแบบโมดูล พร้อมตัวอย่างโค้ด TypeScript ที่เป็นรูปธรรมและคำอธิบายทางเทคนิค

Guards, Interceptors และสถาปัตยกรรมแบบโมดูลของ NestJS สำหรับการสัมภาษณ์ทางเทคนิค

การสัมภาษณ์เทคนิค NestJS มักเน้นไปที่สามเสาหลักของเฟรมเวิร์กอย่างสม่ำเสมอ ได้แก่ Guards, Interceptors และสถาปัตยกรรมแบบโมดูล กลไกเหล่านี้เป็นกระดูกสันหลังของแอปพลิเคชัน NestJS ที่ออกแบบดี และผู้คัดเลือกใช้สิ่งเหล่านี้เพื่อประเมินความเชี่ยวชาญที่แท้จริงในเฟรมเวิร์ก

สิ่งที่ผู้สัมภาษณ์ประเมิน

Guard ควบคุมการเข้าถึง (ใครเข้าได้) Interceptor แปลงข้อมูล (อะไรเข้าและออก) ส่วนสถาปัตยกรรมแบบโมดูลจัดระเบียบทุกอย่าง การเชี่ยวชาญทั้งสามอย่างแสดงถึงความเข้าใจ NestJS ที่ลึกซึ้งเกินกว่าการดำเนินการ CRUD พื้นฐาน

Guards ใน NestJS ทำงานอย่างไรและสถานการณ์สัมภาษณ์ที่พบบ่อย

Guards implement อินเทอร์เฟซ CanActivate และตัดสินว่าคำขอจะไปถึง handler หรือไม่ ต่างจาก middleware ตรงที่ Guards เข้าถึง execution context ของ NestJS (ExecutionContext) ซึ่งทำให้สามารถตัดสินใจเรื่องการอนุญาตโดยอิงตาม metadata ของ handler

คำถามคลาสสิก: "จงเขียน 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 อ่าน metadata ที่ถูกแนบโดย custom decorator ส่วน decorator @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: การแปลง request และ response

Interceptors implement NestInterceptor และใช้ RxJS ในการจัดการสตรีมข้อมูลก่อนและหลัง handler ความสามารถหลักมาจากการเข้าถึง CallHandler ซึ่งเป็นตัวแทนของ pipeline การทำงาน

คำถามที่พบบ่อย: "จงสร้าง logging 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`);
      }),
    );
  }
}

ตัวดำเนินการ tap ของ RxJS สังเกตสตรีมโดยไม่แก้ไข หากต้องการแปลง response ตัวดำเนินการที่เหมาะสมคือ 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,
      })),
    );
  }
}

รูปแบบ response ที่เป็นมาตรฐานนี้ได้คะแนนดีในการสัมภาษณ์: แสดงถึงความสามารถในการคิดเรื่องการออกแบบ API และความสอดคล้องของ response

Guard vs Interceptor vs Middleware

Middleware: การประมวลผล HTTP ดิบ (เหมือน Express) Guard: การตัดสินใจเข้าถึงแบบ boolean Interceptor: การแปลงสตรีม request/response ด้วย RxJS การสับสนระหว่างสามแนวคิดนี้ในการสัมภาษณ์เป็นสัญญาณเตือนทันที

สถาปัตยกรรมแบบโมดูลของ NestJS: สร้างแอปพลิเคชันที่ขยายขนาดได้

สถาปัตยกรรมแบบโมดูลแยกนักพัฒนาระดับ junior ออกจาก senior ในการสัมภาษณ์ NestJS บังคับการจัดระเบียบตามโมดูล แต่คำถามจริงพุ่งเป้าไปที่การตัดสินใจเชิงสถาปัตยกรรม: เมื่อไหร่ควรสร้างโมดูลและจัดการการพึ่งพาระหว่างโมดูลอย่างไร

คำถามขั้นสูง: "คุณจะวางโครงสร้างแอปพลิเคชัน e-commerce ใน 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 import PaymentsModule และ PaymentsModule import OrdersModule NestJS จะล้มเหลวตอนเริ่มทำงาน ทางออกฉุกเฉินคือใช้ forwardRef:

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

คำตอบที่หนักแน่นที่สุดอธิบายว่า forwardRef เป็นการแก้ปัญหาเฉพาะหน้า ไม่ใช่การแก้ที่แท้จริง การแก้ไขที่ถูกต้องคือการ refactor เพื่อแยกโมดูลที่ใช้ร่วมกัน (SharedOrderPaymentModule) หรือใช้ Event Emitter เพื่อแยกการสื่อสารออกจากกัน

พร้อมที่จะพิชิตการสัมภาษณ์ Node.js / NestJS แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

Dynamic Modules และการกำหนดค่าขั้นสูง

Dynamic Modules สร้างโมดูลที่กำหนดค่าได้และนำกลับมาใช้ใหม่ระหว่างโครงการ รูปแบบนี้ปรากฏในการสัมภาษณ์ระดับ senior

คำถาม: "จงสร้างโมดูล cache ที่กำหนดค่าได้พร้อมตัวเลือกแบบไดนามิก"

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 ตัวเลือกผ่าน @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 ที่ inject ได้ ซึ่งเป็นแนวปฏิบัติที่แนะนำสำหรับ production

กับดักการสัมภาษณ์: @Global()

การทำเครื่องหมายโมดูลเป็น @Global() ดูสะดวก แต่การใช้มากเกินไปสร้างการเชื่อมโยงที่มองไม่เห็น การกล่าวในการสัมภาษณ์ว่า @Global() ควรสงวนไว้สำหรับบริการที่ตัดข้าม (config, cache, logger) แสดงถึงวุฒิภาวะเชิงสถาปัตยกรรม

Custom Decorators และการประกอบ Guards

Custom decorators รวม Guards หลายตัวและ metadata ไว้ใน annotation เดียว หัวข้อนี้ปรากฏในตำแหน่ง 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),
  );
}

การใช้งานใน 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);
  }
}

รูปแบบ applyDecorators ทำให้โค้ด controller เรียบง่ายขึ้นและรวมศูนย์ตรรกะการอนุญาต การเสนอแนวทางนี้อย่างเป็นเชิงรุกในการสัมภาษณ์เป็นสัญญาณที่หนักแน่น

บทสรุป

  • Guards ใช้ CanActivate และ Reflector ในการตัดสินใจเข้าถึงตาม metadata ของ handler
  • Interceptors ใช้ประโยชน์จาก RxJS (tap, map) เพื่อแปลงสตรีมก่อนและหลัง handler
  • ลำดับการทำงาน Middleware → Guards → Interceptors → Pipes → Handler เป็นคำถามแทบจะสากล
  • การพึ่งพาแบบวงกลมควรแก้ไขด้วยการ refactor เชิงสถาปัตยกรรม ไม่ใช่ด้วย forwardRef
  • Dynamic Modules (forRoot/forRootAsync) แสดงถึงความสามารถในการสร้างคอมโพเนนต์ที่นำกลับมาใช้ใหม่ได้
  • applyDecorators รวม Guards หลายตัวเข้าเป็น decorator เดียวที่อ่านง่าย
  • ฝึกคำถามเหล่านี้ด้วยโค้ดจริงใน โมดูล NestJS และ Interceptors

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

แท็ก

#nestjs
#guards
#interceptors
#architecture
#interview

แชร์

บทความที่เกี่ยวข้อง