Skip to content
DevRecall
← Roadmap
07

NestJS

drafted

Modules, DI & injection scopes (chi phí request scope), request lifecycle middleware → guard → interceptor → pipe → handler → interceptor → filter, custom decorators, microservices transports, testing, circular deps.

0/46 rated · mastery 0%

Bài học

11 bài · ~122 phút đọc. Học theo thứ tự, rồi làm câu hỏi.
  1. 01~13 phút· 6 câuverify

    NestJS là gì: kiến trúc, module và ranh giới module

    Nest trên Express/Fastify, module là đơn vị đóng gói provider (imports/providers/exports, @Global), dynamic module (forRoot/register/forFeature, ConfigurableModuleBuilder), modular monolith có ranh giới rõ, khi nào chọn Nest thay vì Express và cách chuyển dần một app Express sang Nest.

  2. 02~13 phút· 5 câuverify

    Dependency injection: container, token, custom provider và circular dependency

    Cách Nest đọc design:paramtypes để resolve dependency, injection token (class/string/symbol, vì sao interface không làm token được), useClass/useValue/useFactory/useExisting, đọc hiểu lỗi 'Nest can't resolve dependencies', forwardRef và vì sao dependency có thể chưa sẵn sàng trong constructor, cùng rủi ro dài hạn của legacy decorator.

  3. 03~12 phút· 5 câuverify

    Injection scopes: singleton, request, transient và cái giá của request context

    DEFAULT/REQUEST/TRANSIENT, vì sao request scope lan ngược lên cả chuỗi dependency, đo chi phí thật, vì sao cron/Kafka consumer hỏng khi dùng provider request-scoped, durable providers theo tenant, AsyncLocalStorage (nestjs-cls) và thiết kế multi-tenancy trong Nest.

  4. 04~11 phút· 4 câuverify

    Request lifecycle: middleware, guard, interceptor, pipe, filter chạy theo thứ tự nào

    Thứ tự chính xác của pipeline request trong Nest (kể cả thứ tự global → controller → route trong từng tầng), đã chạy thật trên Nest 12; chọn đúng tầng cho từng yêu cầu; useGlobal* vs APP_* providers và vì sao guard tạo bằng new mất DI.

  5. 05~10 phút· 4 câuverify

    Controller, DTO, ValidationPipe và serialization an toàn

    Controller map route và lấy input thế nào, @Res() làm mất gì, ValidationPipe với whitelist/forbidNonWhitelisted/transform (đo thật: transform một mình không đổi kiểu query), nested DTO, boolean query, và vì sao ClassSerializerInterceptor + @Exclude vẫn làm lộ passwordHash.

  6. 06~11 phút· 6 câuverify

    Guards, custom decorator và authorization trong Nest

    Guard và ExecutionContext, Reflector (createDecorator, getAllAndOverride), @Public/@Roles/@CurrentUser chạy thật (kể cả bug @Public bị @Roles cấp class chặn), JWT bằng Passport hay guard tự viết, RBAC + kiểm tra theo resource cho B2B, và cách mang thiết kế permission middleware từ Express sang Nest.

  7. 07~9 phút· 4 câuverify

    Interceptors (RxJS) và exception filters

    Interceptor bọc handler bằng Observable: timing, envelope, timeout (và vì sao query vẫn chạy sau timeout), CacheInterceptor làm lộ dữ liệu giữa tenant (đo thật) vs cache-aside trong service; exception layer mặc định, filter global map domain error thành response nhất quán, ArgumentsHost cho http/rpc/ws.

  8. 08~10 phút· 4 câuverify

    Config, lifecycle hooks và graceful shutdown

    ConfigModule fail fast bằng Standard Schema (zod) trên Nest 12, typed config với registerAs, thứ tự lifecycle hooks theo cấp module (đo thật), enableShutdownHooks, vì sao app.close() chờ keep-alive vài giây, forceCloseConnections cắt request đang chạy, và trình tự shutdown an toàn trên Kubernetes cho API + Kafka consumer.

  9. 09~12 phút· 5 câuverify

    Microservices trong Nest: transport, hybrid app và Kafka consumer

    @nestjs/microservices: các transport, @MessagePattern (request-response) vs @EventPattern (event), hybrid app HTTP + consumer, lỗi trong context rpc (HttpException thành 'Internal server error'), đo thật trên Kafka: handler ném lỗi chặn cả partition, rebalance do handler chậm, và khi nào nên bọc client Kafka trực tiếp.

  10. 10~9 phút· 4 câuverify

    Testing trong Nest: unit, testing module, e2e và provider có scope

    Ba mức test (new class trực tiếp, Test.createTestingModule + overrideProvider, e2e với createNestApplication + supertest), bẫy e2e xanh nhưng production 400 vì cấu hình nằm trong main.ts (đo thật), overrideGuard và cái giá của nó, resolve() cho provider request/transient, và cách chạy một POC đánh giá Nest cho team.

  11. 11~11 phút· 5 câuverify

    Performance, Fastify, cold start và nâng cấp Nest 11/12

    Nest thêm overhead ở đâu so với Express (đo thật: Express thuần, Nest + Express, Nest + Fastify), chuyển adapter sang Fastify thay đổi những gì, cold start trên serverless, và các thay đổi phá vỡ của Nest 11 (Express 5, cú pháp wildcard, query parser) và Nest 12 (ESM-only, Node, Vitest, Standard Schema, thứ tự hook).

Tóm tắt & cheat sheet của track

TL;DR

  • NestJS là framework opinionated trên Node: module + DI container + decorator, chạy trên HTTP adapter Express (mặc định) hoặc Fastify. Bản chất vẫn là Express/Fastify bên dưới.
  • Nắm chắc request lifecycle: middleware → guards → interceptors (trước) → pipes → handler → interceptors (sau) → exception filters. Nhiều ghi chú trên mạng đặt pipe trước interceptor — sai theo docs chính thức.
  • DI có 3 scope: DEFAULT (singleton), REQUEST, TRANSIENT. Request scope lan ngược lên chuỗi dependency (controller, service phụ thuộc cũng thành request-scoped) → tốn chi phí khởi tạo mỗi request; dùng durable providers hoặc AsyncLocalStorage/CLS khi cần context.
  • Microservices module: transport TCP, Redis, NATS, MQTT, RabbitMQ, Kafka, gRPC, custom; @MessagePattern (request-response) vs @EventPattern (event).
  • Version (verify): Nest 11 dùng Express 5 mặc định cho platform-express (cú pháp route wildcard mới); Nest 12 chuyển core package sang ESM-only và nâng yêu cầu Node.

Khái niệm cốt lõi

Khái niệmMột câuVí dụ ngắn
ModuleĐơn vị đóng gói: imports, controllers, providers, exports@Module({ providers: [OrdersService], exports: [OrdersService] })
ProviderThứ được DI container tạo và inject (class, value, factory)@Injectable() class OrdersService
Injection tokenKhoá tra cứu provider: class, string, symbol@Inject("PAYMENT_CLIENT")
MiddlewareNhư Express middleware, chạy trước route handler, không biết handler nào sẽ chạyLogging, raw body
GuardcanActivate() → cho qua hay không (authz), biết ExecutionContextRolesGuard đọc metadata @Roles()
InterceptorBọc handler (RxJS), chạy trước + sauLogging thời gian, map response, timeout, cache
PipeValidate/transform argument của handlerValidationPipe, ParseIntPipe
Exception filterBắt exception, tạo response@Catch(HttpException)
Custom decoratorMetadata (SetMetadata/Reflector.createDecorator) hoặc param decorator@CurrentUser()
Dynamic moduleModule cấu hình được lúc importConfigModule.forRoot(), forRootAsync

Cơ chế hoạt động

Request lifecycle

  • Guard chạy sau middleware nên biết handler/metadata (context.getHandler()), middleware thì không.
  • Filter được chọn từ mức thấp nhất (route) lên global; filter đã bắt thì exception không truyền tiếp lên filter khác.

DI & scope bubbling

  • Một provider REQUEST ở "đáy" → mọi thứ phụ thuộc vào nó (trực tiếp hoặc gián tiếp) phải tạo lại mỗi request.
  • TRANSIENT không lan kiểu này: mỗi consumer nhận instance riêng.

Bootstrap & shutdown hooks

Trade-offs

Chủ đềLựa chọn ALựa chọn BGhi chú
Request contextREQUEST-scoped provider (inject REQUEST)AsyncLocalStorage / nestjs-clsScope gây chi phí + lan ngược; ALS rẻ hơn nhưng "ngầm"
Global enhancerapp.useGlobalGuards(new X())Provider APP_GUARD trong moduleAPP_* được DI inject dependency; useGlobal* tạo ngoài container
Validationclass-validator + ValidationPipeSchema (zod/Standard Schema) pipeclass-validator dựa decorator + metadata; schema gọn, dùng chung FE
AdapterExpressFastifyFastify nhanh hơn, nhưng middleware Express không dùng trực tiếp được
MicroserviceTransport của Nest (@nestjs/microservices)Dùng client lib trực tiếp (kafkajs, amqplib)Nest trừu tượng hoá, nhưng giấu bớt control (commit offset, retry)
Circular depforwardRef()Refactor tách module/eventforwardRef là băng keo, không phải thiết kế

Pitfalls

  • Nhớ sai thứ tự lifecycle (pipe trước interceptor) → đặt logic ở sai tầng.
  • Provider dùng ở module khác nhưng quên exports hoặc quên imports → "Nest can't resolve dependencies".
  • Request-scoped logger/tenant context inject vào repository → toàn bộ graph thành request-scoped → latency và GC tăng; và không dùng được trong cron, Kafka consumer, WebSocket gateway.
  • app.useGlobalGuards(new AuthGuard()) → guard không có DI (Reflector, ConfigService undefined).
  • ValidationPipe không bật whitelist/forbidNonWhitelisted → mass assignment; không bật transform → @Query() page: number vẫn là string.
  • Nested DTO thiếu @ValidateNested() + @Type(() => X) → object con không được validate.
  • import type cho class dùng ở constructor → mất metadata design:paramtypes → DI không resolve.
  • Quên app.enableShutdownHooks() → onModuleDestroy/onApplicationShutdown không chạy khi nhận SIGTERM.
  • Barrel file (index.ts) gây circular import → dependency undefined lúc decorator chạy.

Cheat sheet

  • Lifecycle: MW → Guard → Interceptor(before) → Pipe → Handler → Interceptor(after) → Filter (khi có lỗi).
  • Custom provider: useClass · useValue · useFactory + inject · useExisting (alias). Async factory = await trước khi app start.
  • APP_GUARD / APP_INTERCEPTOR / APP_PIPE / APP_FILTER để đăng ký global enhancer có DI.
  • Scope: @Injectable({ scope: Scope.REQUEST }); durable: { scope: Scope.REQUEST, durable: true } + ContextIdFactory.apply(strategy).
  • Testing: Test.createTestingModule({...}).overrideProvider(X).useValue(mock).compile(); scoped provider lấy bằng moduleRef.resolve() chứ không phải get().
  • Microservices: @MessagePattern (có response) · @EventPattern (fire-and-forget); hybrid app = app.connectMicroservice() + startAllMicroservices().
  • Circular: forwardRef(() => OtherModule) ở cả hai phía + @Inject(forwardRef(() => OtherService)).

Personal notes (Notion)

Ghi chú cá nhân — có thể sai hoặc lỗi thời, dùng để tham khảo.

Interview Questions

Showing 46 / 46

@Module({
  imports: [DatabaseModule],
  controllers: [OrdersController],
  providers: [OrdersService, OrdersRepository],
  exports: [OrdersService], // repository stays private to this module
})
export class OrdersModule {}
export const PAYMENT_GATEWAY = Symbol("PAYMENT_GATEWAY");
export interface PaymentGateway { charge(amountMinor: number, token: string): Promise<string> }
@Module({ providers: [{ provide: PAYMENT_GATEWAY, useClass: StripeGateway }] })
export class PaymentsModule {}
@Injectable()
export class CheckoutService {
  constructor(@Inject(PAYMENT_GATEWAY) private readonly gateway: PaymentGateway) {}
}
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }));
export class CreateOrderDto {
  @IsString() sku!: string;
  @IsInt() @Min(1) quantity!: number;
}
{
  provide: "REDIS",
  useFactory: async (cfg: ConfigService) => {
    const client = createClient({ url: cfg.getOrThrow("REDIS_URL") });
    await client.connect();
    return client;
  },
  inject: [ConfigService],
}
@Module({})
export class HttpClientModule {
  static register(opts: { baseUrl: string; timeoutMs: number }): DynamicModule {
    return {
      module: HttpClientModule,
      providers: [{ provide: HTTP_OPTS, useValue: opts }, HttpClient],
      exports: [HttpClient],
    };
  }
}
export const Roles = Reflector.createDecorator<string[]>();
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}
  canActivate(ctx: ExecutionContext) {
    const roles = this.reflector.getAllAndOverride(Roles, [ctx.getHandler(), ctx.getClass()]);
    if (!roles?.length) return true;
    const user = ctx.switchToHttp().getRequest().user;
    return roles.some((r) => user?.roles?.includes(r));
  }
}
export const CurrentUser = createParamDecorator((_: unknown, ctx: ExecutionContext) =>
  ctx.switchToHttp().getRequest().user);
@Injectable()
export class TimingInterceptor implements NestInterceptor {
  intercept(ctx: ExecutionContext, next: CallHandler) {
    const start = performance.now();
    return next.handle().pipe(
      timeout(5_000),
      catchError((e) => throwError(() => (e instanceof TimeoutError ? new RequestTimeoutException() : e))),
      finalize(() => logger.info({ route: ctx.getHandler().name, ms: performance.now() - start })),
    );
  }
}
@Module({ providers: [PaymentsService] })
export class PaymentsModule {}

@Module({
  imports: [PaymentsModule],
  providers: [OrdersService],
})
export class OrdersModule {}
@Injectable()
export class UsersService {
  constructor(@Inject(forwardRef(() => AuthService)) private auth: AuthService) {
    this.auth.registerHook(this); // called in the constructor
  }
}
@Injectable()
export class AuthService {
  constructor(@Inject(forwardRef(() => UsersService)) private users: UsersService) {}
}
// main.ts
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard(undefined as any)); // colleague "fixed" a compile error
// roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}
  canActivate(ctx: ExecutionContext) {
    const roles = this.reflector.getAllAndOverride(Roles, [ctx.getHandler(), ctx.getClass()]);
    return !roles || matches(roles, ctx);
  }
}
class ListOrdersQuery {
  @IsOptional() @IsInt() page?: number;
}
class AddressDto { @IsString() line1!: string; @IsPostalCode("any") zip!: string }
class CreateOrderDto { address!: AddressDto }

@Get() list(@Query() q: ListOrdersQuery) { return this.svc.list((q.page ?? 1) * 10 - 10); }
// main.ts
app.useGlobalPipes(new ValidationPipe());