Bỏ qua để đến nội dung
Knowledge Hub

Request lifecycle trong NestJS: ai chạy trước, ai chặn request, lỗi rơi vào đâu Trung cấp

Thời gian đọc ước tính: 8 phút

Interceptor log id là chuỗi "42" trong khi handler nhận số 42. Hoặc request bị guard từ chối nhưng không hề xuất hiện trong metric đặt ở interceptor. Cả hai đều là hệ quả của lifecycle: cần biết thành phần nào đã chạy, dữ liệu đang ở dạng nào, và ai bắt lỗi.

# Lớp Mức áp dụng và thứ tự Dừng được request?
1 Middleware Global trước, rồi middleware gắn theo module Có
2 Guard global, controller, route Có (trả false hoặc ném lỗi)
3 Interceptor (vào) global, controller, route Có (không gọi next.handle())
4 Pipe global, controller, route, rồi pipe của từng tham số Có (ném lỗi)
5 Handler (và service)
6 Interceptor (ra) route, controller, global (ngược chiều vào)
7 Exception filter route, controller, global (từ thấp lên cao) Chỉ xử lý lỗi chưa được bắt

Trong cùng một mức, thứ tự là thứ tự khai báo (guard 1 chạy trước guard 2). Đây là thứ tự của request thành công; request lỗi rẽ nhánh như hình dưới.

Đang tải diagram…

Đường đi Lớp đã chạy Lớp bị bỏ qua
Thành công Middleware, guard, interceptor vào, pipe, handler, interceptor ra Filter
Guard từ chối Middleware, guard Interceptor, pipe, handler; metric ở interceptor không thấy request này
Pipe ném lỗi Middleware, guard, interceptor vào, pipe Handler và phần tap chiều ra

Demo order-demo.ts đặt guard, interceptor, pipe và filter ở cả ba mức (global qua APP_*, controller, route), mỗi cái log tên. Output thật trên Nest 11:

Thành công: GET /orders/42
guard global
guard controller
guard route
interceptor global (vào)
interceptor controller (vào)
interceptor route (vào)
pipe global: "42"
pipe controller: 42
pipe route: 42
handler nhận number 42
interceptor route (ra)
interceptor controller (ra)
interceptor global (ra)
-> 200 {"id":42}

Đọc ra ba quy tắc:

  1. Guard, interceptor (vào), pipe chạy global, controller, route.
  2. Interceptor (ra) chạy ngược: route, controller, global. Đó là cấu trúc lồng như hành tây.
  3. Pipe nối tiếp nhau: pipe global nhận chuỗi "42", trả ra số 42 thì pipe controller và route nhận số.

Hai nhánh lỗi cùng demo (các dòng lặp được rút gọn thành ...):

Controller guard từ chối: GET /orders/42
guard global
guard controller
filter route bắt: Forbidden resource
-> 403 {"by":"route"}
Pipe route ném lỗi: GET /orders/abc
guard global ... guard route
interceptor global, controller, route (vào)
pipe global: "abc"
pipe controller: "abc"
pipe route: "abc"
filter route bắt: id phải là số
-> 400 {"by":"route"}

Filter resolve từ thấp lên cao: route, controller rồi mới global. Ở ví dụ guard từ chối, filter của route vẫn là filter bắt lỗi, dù lỗi sinh ra ở guard của controller, vì route đã được xác định.

Một lỗi chỉ được một filter xử lý; filter đã bắt thì không chuyển sang filter khác. Lỗi ném ở middleware thì chỉ filter global áp dụng, vì lúc đó chưa chọn route.

Lớp Thấy id dưới dạng Ghi chú
Middleware, guard, interceptor (vào) Chuỗi "42" trong req.params.id Pipe chưa chạy
Pipe Nhận "42", trả 42 Giá trị trả về thành tham số handler
Handler Số 42 req.params.id vẫn là chuỗi; pipe không ghi ngược
Interceptor (ra) Kết quả handler Không thấy tham số đã transform

Hệ quả: guard dùng tham số URL để quyết định quyền phải tự validate dữ liệu đó, không được mặc định rằng pipe đã làm.

Thay pipe viết tay bằng ValidationPipe kèm DTO (cần class-validator và class-transformer; đoạn này theo tài liệu Validation, chưa chạy trong repo vì dependency chưa cài ở examples):

export class CreateOrderDto {
@IsString() sku: string;
@IsInt() @Min(1) quantity: number;
}
// main.ts
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
// controller
@Post()
create(@Body() dto: CreateOrderDto) {}
@Get(':id')
get(@Param('id', ParseIntPipe) id: number) {}

transform: true biến body thành instance CreateOrderDto; whitelist: true loại field không khai báo; ParseIntPipe đổi "42" thành 42 và trả 400 khi không phải số. Vì pipe chạy sau guard, quyền phải kiểm tra trước, validation sau.

Cần Đặt ở Vì sao
Access log, request ID cho mọi request, kể cả bị từ chối Middleware Chạy sớm nhất, không phụ thuộc route
Quyền dựa trên metadata của route Guard Có ExecutionContext
Validate và đổi kiểu tham số Pipe Ngay trước handler
Đo thời gian, biến đổi response Interceptor, chốt bằng finalize Có cả chiều vào và ra, bắt cả nhánh lỗi
Chuẩn hoá response lỗi Exception filter Chỉ chạy với lỗi chưa bắt
  1. Kiểm tra quyền ở guard, validation ở pipe; guard không giả định tham số đã được pipe làm sạch.
  2. Dùng ValidationPipe global với whitelist và DTO thay cho validate viết tay.
  3. Đo thời gian bằng finalize trong interceptor để gồm nhánh lỗi; đo toàn bộ request ở middleware.
  4. Biết filter nào bắt lỗi: đặt filter chung ở global, filter đặc thù ở controller hoặc route; lỗi từ middleware chỉ có global filter.
  5. Khi có guard hay interceptor ở nhiều mức, log tên để thấy thứ tự; nhớ interceptor (ra) chạy ngược.
  6. Mở context cho ALS ở middleware để guard và interceptor đọc được.

Tự kiểm tra: (1) Guard trả false thì pipe có chạy không, và interceptor thấy gì? (2) Có interceptor ở global, controller và route: thứ tự chiều response là gì? (3) Lỗi từ guard của controller được filter nào bắt trước?

Chạy demo:

Terminal window
npx --prefix examples ts-node examples/src/nestjs-request-lifecycle/order-demo.ts
# hoặc server mẫu: cd examples && npm ci && npm run lifecycle-demo