Skip to main content

NestJS chuyên sâu

Trang này đi sâu hơn NestJS handbook: thứ tự các thành phần chạy trong một request, cách DI quản lý vòng đời provider, và các vấn đề thực tế khi đưa NestJS vào microservices, real-time và production.

Vòng đời request​

BướcDùng đểVí dụ
MiddlewareLogic chung trước routing, có thể sửa req/reslogging, helmet, CORS
GuardsQuyết định request có được đi tiếp hay không; trả false → 403AuthGuard('jwt'), RolesGuard
Interceptors (trước)Bọc quanh handler: đo thời gian, cache, ghi đè hành viCacheInterceptor, timeout
PipesBiến đổi và validate tham số (body, params, query)ValidationPipe, ParseIntPipe
Controller → ServiceXử lý request, logic nghiệp vụ
Interceptors (sau)Biến đổi kết quả trả vềbọc { data }, serialize
Exception filtersBắt exception ở bất kỳ bước nào, định dạng response lỗiHttpExceptionFilter

Guards, interceptors, pipes và filters đều gắn được ở 3 mức: global (app.useGlobal*() hoặc provider APP_*), controller và method (@UseGuards(), @UseInterceptors()…). Khi request đi vào, chúng chạy từ global → controller → method; interceptors sau handler và filters chạy theo chiều ngược lại.

Dependency injection & provider scopes​

Khai báo dependency trong constructor, Nest IoC container sẽ tạo và inject instance:

users.controller.ts
@Controller("users")
export class UsersController {
constructor(private readonly usersService: UsersService) {}
}
ScopeInstanceKhi nào dùng
DEFAULT (singleton)Một instance cho cả ứng dụngHầu hết service — nhanh nhất, nên giữ stateless
REQUESTMột instance mới cho mỗi requestCần dữ liệu riêng của request (tenant, user)
TRANSIENTMột instance mới mỗi lần được injectHelper có state riêng cho từng consumer (logger có context)
@Injectable({ scope: Scope.REQUEST })
export class TenantService {
constructor(@Inject(REQUEST) private readonly request: Request) {}
}
warning

Scope REQUEST lan ngược lên chuỗi phụ thuộc: controller inject một provider request-scoped cũng trở thành request-scoped và bị tạo lại mỗi request. Dùng có chọn lọc vì ảnh hưởng hiệu năng.

Ngoài class, provider còn có thể là giá trị, class thay thế hoặc factory:

app.module.ts
providers: [
{ provide: "API_URL", useValue: "https://api.example.com" },
{ provide: PaymentGateway, useClass: process.env.NODE_ENV === "test" ? FakeGateway : StripeGateway },
{
provide: "REDIS",
useFactory: (config: ConfigService) => new Redis(config.get("REDIS_URL")),
inject: [ConfigService],
},
],

Validation với DTO​

DTO (Data Transfer Object) là class mô tả dữ liệu đi qua mạng. Decorator của class-validator khai báo luật, ValidationPipe thực thi chúng.

create-post.dto.ts
import { IsString, IsNotEmpty, MinLength } from "class-validator";

export class CreatePostDto {
@IsString()
@MinLength(5)
title: string;

@IsString()
@IsNotEmpty()
content: string;
}
main.ts
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // bỏ các field không có decorator
forbidNonWhitelisted: true, // ...hoặc trả 400 nếu có field lạ
transform: true, // chuyển plain object thành instance của DTO, ép kiểu params
}),
);

Khi validate thất bại, pipe ném BadRequestException kèm danh sách lỗi theo từng field.

Các gói phổ biến​

Bảng tổng hợp có trong handbook. Cách tích hợp từng gói:

// app.module.ts
@Module({
imports: [ConfigModule.forRoot({ isGlobal: true })],
})
export class AppModule {}

// any.service.ts
constructor(private readonly config: ConfigService) {
const dbHost = this.config.get<string>("DATABASE_HOST");
}

Microservices​

Package @nestjs/microservices trừu tượng hóa transport (TCP, Redis, NATS, RabbitMQ, Kafka, gRPC…): cùng một @MessagePattern() / @EventPattern() chạy được trên nhiều transport khác nhau.

Chọn kiểu giao tiếp​

RESTgRPCKafka / RabbitMQ / NATS
KiểuĐồng bộ, request-responseĐồng bộ (RPC), streaming hai chiềuBất đồng bộ, event / pub-sub
Độ trễTrung bìnhRất thấp (HTTP/2 + Protobuf)Thấp đến trung bình
Độ tin cậyPhải tự retryPhải tự retryMessage được broker lưu bền, consumer xử lý lại được
Dùng khiAPI công khai, đơn giảnGọi nội bộ giữa service, cần nhanhTách rời service, event-driven, data pipeline

Database per service​

Mỗi service sở hữu database riêng; service khác chỉ đọc qua API hoặc event của nó. Đổi lại được ghép nối lỏng, tự chọn loại DB phù hợp (SQL cho giao dịch, NoSQL cho profile) và scale độc lập — nhưng phải xử lý giao dịch phân tán và tính nhất quán dữ liệu (xem bên dưới).

Scaling & logging​

Scale dọc (up)Scale ngang (out)
Cách làmTăng CPU/RAM cho một máyThêm instance sau load balancer
Giới hạnTrần phần cứng, có downtime khi nâng cấpGần như không giới hạn
Chi phí ở quy mô lớnTăng nhanhTuyến tính hơn
Yêu cầuKhôngService phải stateless

Với nhiều service, logging tập trung là bắt buộc (ELK stack, Grafana Loki). Sinh một correlation ID (trace ID) ở điểm vào, truyền qua mọi lời gọi service và ghi vào mọi log để lần theo một request xuyên hệ thống. OpenTelemetry làm việc này tự động.

Thách thức thường gặp​

Giao dịch phân tán → Saga

Tạo đơn hàng cần trừ tồn kho (product service) và thanh toán (payment service) — không có transaction ACID xuyên service.

Saga chia thao tác thành chuỗi transaction cục bộ: mỗi bước cập nhật DB của một service rồi phát event kích hoạt bước tiếp theo. Nếu một bước lỗi, các transaction bù (compensating) hoàn tác những bước đã xong (ví dụ: hoàn lại tồn kho).

Truy vấn xuyên service → API composition hoặc CQRS

Không thể JOIN giữa các database.

  • API composition: một aggregator hoặc API gateway gọi nhiều service rồi ghép kết quả.
  • CQRS: tách model ghi (command) và đọc (query). Model đọc là view đã denormalize, cập nhật bằng cách lắng nghe event từ nhiều service — xem @nestjs/cqrs.
Một service sập → Circuit breaker

Circuit breaker bọc lời gọi đến service khác. Khi lỗi liên tục, mạch mở: các lời gọi sau thất bại ngay, không chờ timeout. Sau một khoảng, mạch nửa mở và thử vài lời gọi; thành công thì đóng lại. Trong Node.js có thể dùng opossum.

WebSockets​

Trong NestJS, WebSocket server là gateway — class gắn @WebSocketGateway(), chạy trên Socket.IO (mặc định) hoặc ws.

chat.gateway.ts
@WebSocketGateway({ cors: { origin: ["https://app.example.com"] } })
export class ChatGateway implements OnGatewayConnection {
@WebSocketServer()
server: Server;

constructor(private readonly jwt: JwtService) {}

async handleConnection(client: Socket) {
try {
// Client gửi token khi kết nối: io(url, { auth: { token } })
client.data.user = await this.jwt.verifyAsync(client.handshake.auth.token);
} catch {
client.disconnect();
}
}

@SubscribeMessage("joinRoom")
handleJoinRoom(@MessageBody() room: string, @ConnectedSocket() client: Socket) {
client.join(room);
client.emit("joinedRoom", room);
}

@SubscribeMessage("sendMessage")
handleMessage(@MessageBody() data: { room: string; message: string }, @ConnectedSocket() client: Socket) {
this.server.to(data.room).emit("newMessage", { user: client.data.user.sub, text: data.message });
}
}
  • Xác thực: dùng chung JWT với REST API. Verify ở handleConnection để từ chối ngay khi bắt tay, hoặc viết guard cho từng message (context.switchToWs().getClient()).
  • Scale nhiều instance: user A ở instance 1 không nhận được message phát từ instance 2. Dùng Redis adapter (@socket.io/redis-adapter) để các instance phát event cho nhau qua Redis pub/sub; load balancer cần sticky session nếu Socket.IO dùng long-polling.
  • Kết nối chết: Socket.IO tự gửi heartbeat ping/pong (pingInterval, pingTimeout) và đóng kết nối không phản hồi, nên không cần tự xử lý.

Bảo mật​

Xác thực & phân quyền​

Phân quyền theo vai trò bằng một guard đọc metadata:

roles.guard.ts
export const Roles = Reflector.createDecorator<string[]>();

@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}

canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get(Roles, context.getHandler());
if (!roles) return true;
const { user } = context.switchToHttp().getRequest();
return roles.some((role) => user?.roles?.includes(role));
}
}

// Dùng: @Roles(["admin"]) @UseGuards(AuthGuard("jwt"), RolesGuard)
tip

Lưu token ở cookie HttpOnly an toàn hơn localStorage trước XSS, nhưng khi đó phải chống CSRF (bên dưới).

Các mối đe dọa phổ biến​

Mối đe dọaGiải pháp trong NestJS
Brute-force, DoS@nestjs/throttler với ThrottlerGuard global; giới hạn chặt hơn cho /auth/login bằng @Throttle()
Gọi API từ domain khácapp.enableCors({ origin: ["https://app.example.com"], credentials: true }) — không dùng origin: "*" cùng cookie
HTTP header không an toànapp.use(helmet()) — đặt Strict-Transport-Security, X-Content-Type-Options, CSP…
CSRF (khi xác thực bằng cookie)Cookie SameSite=Lax/Strict và CSRF token, ví dụ csrf-csrf (csurf đã ngừng phát triển). API dùng header Authorization: Bearer không bị CSRF
Dữ liệu đầu vào độc hạiValidationPipe với whitelist: true, không truyền thẳng body vào query