@Module({
imports: [DatabaseModule],
controllers: [OrdersController],
providers: [OrdersService, OrdersRepository],
exports: [OrdersService], // repository stays private to this module
})
export class OrdersModule {}
NestJS
draftedModules, 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.- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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~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 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ệm | Một câu | Ví dụ ngắn |
|---|---|---|
| Module | Đơn vị đóng gói: imports, controllers, providers, exports | @Module({ providers: [OrdersService], exports: [OrdersService] }) |
| Provider | Thứ được DI container tạo và inject (class, value, factory) | @Injectable() class OrdersService |
| Injection token | Khoá tra cứu provider: class, string, symbol | @Inject("PAYMENT_CLIENT") |
| Middleware | Như Express middleware, chạy trước route handler, không biết handler nào sẽ chạy | Logging, raw body |
| Guard | canActivate() → cho qua hay không (authz), biết ExecutionContext | RolesGuard đọc metadata @Roles() |
| Interceptor | Bọc handler (RxJS), chạy trước + sau | Logging thời gian, map response, timeout, cache |
| Pipe | Validate/transform argument của handler | ValidationPipe, ParseIntPipe |
| Exception filter | Bắt exception, tạo response | @Catch(HttpException) |
| Custom decorator | Metadata (SetMetadata/Reflector.createDecorator) hoặc param decorator | @CurrentUser() |
| Dynamic module | Module cấu hình được lúc import | ConfigModule.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 A | Lựa chọn B | Ghi chú |
|---|---|---|---|
| Request context | REQUEST-scoped provider (inject REQUEST) | AsyncLocalStorage / nestjs-cls | Scope gây chi phí + lan ngược; ALS rẻ hơn nhưng "ngầm" |
| Global enhancer | app.useGlobalGuards(new X()) | Provider APP_GUARD trong module | APP_* được DI inject dependency; useGlobal* tạo ngoài container |
| Validation | class-validator + ValidationPipe | Schema (zod/Standard Schema) pipe | class-validator dựa decorator + metadata; schema gọn, dùng chung FE |
| Adapter | Express | Fastify | Fastify nhanh hơn, nhưng middleware Express không dùng trực tiếp được |
| Microservice | Transport 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 dep | forwardRef() | Refactor tách module/event | forwardRef 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
exportshoặc quênimports→ "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).ValidationPipekhông bậtwhitelist/forbidNonWhitelisted→ mass assignment; không bậttransform→@Query() page: numbervẫn là string.- Nested DTO thiếu
@ValidateNested()+@Type(() => X)→ object con không được validate. import typecho class dùng ở constructor → mất metadatadesign:paramtypes→ DI không resolve.- Quên
app.enableShutdownHooks()→onModuleDestroy/onApplicationShutdownkhông chạy khi nhận SIGTERM. - Barrel file (
index.ts) gây circular import → dependencyundefinedlú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ằngmoduleRef.resolve()chứ không phảiget(). - Microservices:
@MessagePattern(có response) ·@EventPattern(fire-and-forget); hybrid app =app.connectMicroservice()+startAllMicroservices(). - Circular:
forwardRef(() => OtherModule)ở cả hai phía +@Inject(forwardRef(() => OtherService)).
References
- NestJS — Request lifecycle ↗
- NestJS — Providers ↗
- NestJS — Modules ↗
- NestJS — Custom providers ↗
- NestJS — Injection scopes ↗
- NestJS — Circular dependency ↗
- NestJS — Lifecycle events ↗
- NestJS — Dynamic modules ↗
- NestJS — Testing ↗
- NestJS — Guards ↗
- NestJS — Interceptors ↗
- NestJS — Pipes / Validation ↗
- NestJS — Custom decorators ↗
- NestJS — Microservices overview ↗
- NestJS — Kafka transport ↗
- NestJS — Migration guide ↗
Interview Questions
Showing 46 / 46
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());