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
Cần đọc trước
Vấn đề
Phần tiêu đề “Vấn đề”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.
Bản đồ nhanh
Phần tiêu đề “Bản đồ nhanh”| # | 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.
Ba đường đi
Phần tiêu đề “Ba đường đ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 |
Chạy thật: global, controller, route
Phần tiêu đề “Chạy thật: global, controller, route”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:
- Guard, interceptor (vào), pipe chạy global, controller, route.
- Interceptor (ra) chạy ngược: route, controller, global. Đó là cấu trúc lồng như hành tây.
- Pipe nối tiếp nhau: pipe global nhận chuỗi
"42", trả ra số42thì 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.
Dữ liệu qua từng lớp
Phần tiêu đề “Dữ liệu qua từng lớp”| 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.
Validation đúng cách
Phần tiêu đề “Validation đúng cách”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.tsapp.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.
Chọn lớp cho chức năng mới
Phần tiêu đề “Chọn lớp cho chức năng mới”| 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 |
Ví dụ thực tế
Phần tiêu đề “Ví dụ thực tế”Checklist production
Phần tiêu đề “Checklist production”- Kiểm tra quyền ở guard, validation ở pipe; guard không giả định tham số đã được pipe làm sạch.
- Dùng
ValidationPipeglobal vớiwhitelistvà DTO thay cho validate viết tay. - Đo thời gian bằng
finalizetrong interceptor để gồm nhánh lỗi; đo toàn bộ request ở middleware. - 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.
- Khi có guard hay interceptor ở nhiều mức, log tên để thấy thứ tự; nhớ interceptor (ra) chạy ngược.
- Mở context cho ALS ở middleware để guard và interceptor đọc được.
Sai lầm thường gặp
Phần tiêu đề “Sai lầm thường gặp”Tóm tắt
Phần tiêu đề “Tóm tắt”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:
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