☰ Mục lục series
Tài liệu NestJS tiếng Việt
  1. Nhập môn (Overview)
    1. Giới thiệu
    2. First steps
    3. Controllers
    4. Providers
    5. Modules
    6. Middleware
    7. Exception filters
    8. Pipes
    9. Guards
    10. Interceptors
    11. Custom decorators
  2. Nền tảng (Fundamentals)
    1. Custom providers
    2. Asynchronous providers
    3. Dynamic modules
    4. Injection scopes
    5. Circular dependency
    6. Module reference
    7. Lazy-loading modules
    8. Execution context
    9. Lifecycle events
    10. Discovery service
    11. Platform agnosticism
    12. Testing
  3. Kỹ thuật (Techniques)
    1. Configuration
    2. Database (SQL)
    3. MongoDB
    4. Validation
    5. Caching
    6. Serialization
    7. Versioning
    8. Task scheduling
    9. Queues
    10. Logging
    11. Cookies
    12. Events
    13. Compression
    14. File upload
    15. Streaming files
    16. HTTP module
    17. Session
    18. MVC
    19. Performance (Fastify)
    20. Server-Sent Events
  4. Bảo mật (Security)
    1. Authentication
    2. Authorization
    3. Encryption & Hashing
    4. Helmet
    5. CORS
    6. CSRF
    7. Rate limiting
    8. Vòng đời request
  5. OpenAPI (Swagger)
    1. Introduction
    2. Types and parameters
    3. Operations
    4. Security
    5. Mapped types
    6. Decorators
    7. CLI plugin
    8. Other features
  6. Recipes
    1. REPL
    2. CRUD generator
    3. SWC (fast compiler)
    4. Passport (auth)
    5. Hot reload
    6. MikroORM
    7. Router module
    8. Health checks (Terminus)
    9. CQRS
    10. Prisma
    11. Serve static
    12. Commander
    13. Async local storage
  7. WebSockets
    1. Gateways
    2. Exception filters
    3. Pipes
    4. Guards
    5. Interceptors
    6. Adapters
  8. Microservices
    1. Overview
    2. Redis
    3. MQTT
    4. NATS
    5. RabbitMQ
    6. Kafka
    7. gRPC
    8. Custom transporters
    9. Exception filters
    10. Pipes
    11. Pre-request hooks
    12. Guards
    13. Interceptors
  9. GraphQL
    1. Quick start
    2. Resolvers
    3. Mutations
    4. Subscriptions
    5. Scalars
    6. Directives
    7. Interfaces
    8. Unions and enums
    9. Field middleware
    10. Mapped types
    11. Plugins
    12. Complexity
    13. Extensions
    14. CLI plugin
    15. Generating SDL
    16. Sharing models
    17. Other features
    18. Federation
  10. CLI & triển khai
    1. CLI overview
    2. Workspaces
    3. Libraries
    4. Usage
    5. Scripts
    6. Deployment
    7. Standalone apps
  11. FAQ
    1. Serverless
    2. HTTP adapter
    3. Keep-Alive connections
    4. Global path prefix
    5. Raw body
    6. Hybrid application
    7. HTTPS & multiple servers
    8. Common errors
  12. Phụ lục
    1. Bảng thuật ngữ
Bảo mật (Security)

Request lifecycle (Vòng đời request)

Bản dịch từ docs.nestjs.com/faq/request-lifecycle

Ứng dụng Nest xử lý request và tạo response theo một trình tự mà chúng ta gọi là vòng đời request (request lifecycle). Với việc dùng middleware, pipe, guard, và interceptor, sẽ khá khó để xác định chính xác một đoạn code chạy ở đâu trong vòng đời request, đặc biệt khi các component ở cấp global, controller, và route cùng tham gia. Nói chung, một request đi qua middleware đến guard, rồi đến interceptor, rồi đến pipe, và cuối cùng trở lại interceptor trên đường về (khi response được tạo ra).

Ghi chú người dịch Nếu bạn quen Express middleware chain hoặc Laravel middleware/kernel, đây chính là bức tranh tổng quát hơn của Nest: middleware của Express chỉ là một lát cắt nhỏ trong toàn bộ pipeline này — guard, interceptor, pipe đều là những lớp bổ sung mà Nest thêm vào phía trên Express/Fastify.

Middleware

Middleware được thực thi theo một trình tự cụ thể. Đầu tiên, Nest chạy middleware được bind ở phạm vi global (ví dụ middleware được bind bằng app.use), sau đó chạy middleware bind theo module, các middleware này được xác định dựa trên path. Middleware chạy tuần tự theo đúng thứ tự chúng được bind, tương tự cách middleware hoạt động trong Express. Trong trường hợp middleware được bind ở nhiều module khác nhau, middleware bind ở root module sẽ chạy trước, sau đó middleware sẽ chạy theo thứ tự mà các module được thêm vào array imports.

Guards

Việc thực thi guard bắt đầu từ guard global, sau đó đến guard ở controller, và cuối cùng đến guard ở route. Giống như middleware, guard chạy theo thứ tự chúng được bind. Ví dụ:

@UseGuards(Guard1, Guard2)
@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @UseGuards(Guard3)
  @Get()
  getCats(): Cats[] {
    return this.catsService.getCats();
  }
}

Guard1 sẽ chạy trước Guard2, và cả hai sẽ chạy trước Guard3.

Gợi ý Khi nói về bind ở phạm vi global so với bind ở controller hoặc local, sự khác biệt nằm ở nơi component (guard hoặc component khác) được bind. Nếu bạn dùng app.useGlobalGuards() hoặc cung cấp component thông qua module, nó được bind ở phạm vi global. Ngược lại, nó được bind ở controller nếu decorator đặt trước class controller, hoặc bind ở route nếu decorator đặt trước khai báo route.

Interceptors

Interceptor, về cơ bản, tuân theo cùng pattern như guard, nhưng có một điểm khác: vì interceptor trả về RxJS Observable, các observable này sẽ được resolve theo kiểu vào sau ra trước (first in last out). Vì vậy, request đi vào sẽ đi qua trình tự resolve tiêu chuẩn: global, controller, route level, nhưng phía response của request (tức là sau khi trả về từ method handler của controller) sẽ được resolve theo thứ tự route → controller → global. Ngoài ra, bất kỳ error nào được throw từ pipe, controller, hoặc service đều có thể được đọc trong operator catchError của interceptor.

Ghi chú người dịch “First in last out” ở đây giống cơ chế middleware “onion” của Koa/Express (await next() rồi code sau đó chạy khi response đi ngược lại) — interceptor bọc quanh phần còn lại của pipeline giống một lớp vỏ hành, chứ không chạy tuyến tính một chiều như guard hay pipe.

Pipes

Pipe tuân theo trình tự bind tiêu chuẩn global → controller → route, cùng nguyên tắc vào trước ra trước (first in first out) đối với tham số của @UsePipes(). Tuy nhiên, ở cấp route parameter, nếu bạn có nhiều pipe cùng chạy, chúng sẽ chạy theo thứ tự từ parameter cuối cùng có pipe đến parameter đầu tiên. Điều này cũng áp dụng cho pipe ở cấp route và cấp controller. Ví dụ, nếu chúng ta có controller sau:

@UsePipes(GeneralValidationPipe)
@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @UsePipes(RouteSpecificPipe)
  @Patch(':id')
  updateCat(
    @Body() body: UpdateCatDTO,
    @Param() params: UpdateCatParams,
    @Query() query: UpdateCatQuery,
  ) {
    return this.catsService.updateCat(body, params, query);
  }
}

thì GeneralValidationPipe sẽ chạy cho query, rồi params, rồi cho object body, trước khi chuyển sang RouteSpecificPipe, pipe này cũng theo cùng thứ tự đó. Nếu có pipe riêng cho từng parameter cụ thể, chúng sẽ chạy (một lần nữa, từ parameter cuối đến đầu) sau khi pipe ở cấp controller và route đã chạy.

Ghi chú người dịch Thứ tự “từ parameter cuối về đầu” khá dễ gây nhầm khi debug — nếu bạn có pipe log lỗi validation ở cả @Body() và @Query(), đừng ngạc nhiên khi thấy @Query() báo lỗi trước @Body() dù body khai trước trong signature.

Filters

Filter là component duy nhất không resolve theo thứ tự global trước. Ngược lại, filter resolve từ cấp thấp nhất có thể, nghĩa là việc thực thi bắt đầu từ filter bind ở route, rồi đến cấp controller, và cuối cùng là filter global. Lưu ý rằng exception không thể được truyền từ filter này sang filter khác; nếu một filter ở cấp route đã bắt được exception, filter ở cấp controller hoặc global sẽ không thể bắt cùng exception đó. Cách duy nhất để đạt được hiệu ứng tương tự là dùng kế thừa (inheritance) giữa các filter.

Gợi ý Filter chỉ được thực thi khi có exception không được bắt (uncaught) xảy ra trong quá trình xử lý request. Exception đã được bắt, chẳng hạn bằng try/catch, sẽ không kích hoạt Exception Filter chạy. Ngay khi gặp một exception không được bắt, phần còn lại của vòng đời sẽ bị bỏ qua và request sẽ nhảy thẳng đến filter. Exception được throw từ middleware cũng được lớp exception xử lý, nhưng chỉ exception filter ở phạm vi global mới áp dụng được, vì middleware chạy trước khi route handler được chọn.

Ghi chú người dịch Đây là lý do một try/catch “nuốt” lỗi trong service của bạn rồi return giá trị mặc định sẽ không bao giờ để ExceptionFilter xử lý được lỗi đó — với Nest, exception filter chỉ là lưới an toàn cho lỗi bạn không tự xử lý, không phải hook chạy sau mọi lỗi.

Summary (Tổng kết)

Nhìn chung, vòng đời request có dạng như sau:

  1. Request đến (Incoming request)
  2. Middleware
    • 2.1. Middleware bind ở phạm vi global
    • 2.2. Middleware bind theo module
  3. Guards
    • 3.1 Guard global
    • 3.2 Guard controller
    • 3.3 Guard route
  4. Interceptors (trước khi vào controller)
    • 4.1 Interceptor global
    • 4.2 Interceptor controller
    • 4.3 Interceptor route
  5. Pipes
    • 5.1 Pipe global
    • 5.2 Pipe controller
    • 5.3 Pipe route
    • 5.4 Pipe route parameter
  6. Controller (method handler)
  7. Service (nếu có)
  8. Interceptors (sau khi request hoàn tất)
    • 8.1 Interceptor route
    • 8.2 Interceptor controller
    • 8.3 Interceptor global
  9. Exception filters
    • 9.1 route
    • 9.2 controller
    • 9.3 global
  10. Response trả về client (Server response)

Nội dung gốc: NestJS Documentation © Kamil Myśliwiec, giấy phép MIT. Bản dịch và ghi chú: Lê Văn Dũng.