☰ 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ữ
Nhập môn (Overview)

Custom route decorators

Bản dịch từ docs.nestjs.com/custom-decorators

Nest được xây dựng xoay quanh một tính năng ngôn ngữ gọi là decorator. Decorator là một khái niệm quen thuộc trong nhiều ngôn ngữ lập trình phổ biến, nhưng trong thế giới JavaScript, chúng vẫn còn khá mới. Để hiểu rõ hơn cách decorator hoạt động, chúng tôi khuyến khích đọc bài viết này. Sau đây là một định nghĩa đơn giản:

An ES2016 decorator is an expression which returns a function and can take a target, name and property descriptor as arguments. You apply it by prefixing the decorator with an @ character and placing this at the very top of what you are trying to decorate. Decorators can be defined for either a class, a method or a property.

Param decorators

Nest cung cấp một tập hợp các param decorator hữu ích mà bạn có thể dùng cùng với HTTP route handler. Dưới đây là danh sách các decorator được cung cấp và object Express (hoặc Fastify) thuần mà chúng đại diện

@Request(), @Req() req
@Response(), @Res() res
@Next() next
@Session() req.session
@Param(param?: string) req.params / req.params[param]
@Body(param?: string) req.body / req.body[param]
@Query(param?: string) req.query / req.query[param]
@Headers(param?: string) req.headers / req.headers[param]
@Ip() req.ip
@HostParam() req.hosts

Ngoài ra, bạn còn có thể tạo custom decorator của riêng mình. Tại sao điều này hữu ích?

Trong thế giới node.js, việc gắn property vào object request là chuyện phổ biến. Sau đó bạn tự tay trích xuất chúng trong mỗi route handler, dùng code như sau:

const user = req.user;

Để làm code của bạn dễ đọc và rõ ràng hơn, bạn có thể tạo một decorator @User() và tái sử dụng nó trong tất cả controller của mình.

Ghi chú người dịch Về bản chất, createParamDecorator chỉ là một lớp bọc quanh đúng dòng req.user mà bạn vẫn viết tay trong Express - nó không “phát hiện” gì thêm ngoài request hiện tại. Cái bạn đổi được là chỗ viết logic trích xuất: viết một lần, dùng lại bằng @User() ở mọi handler thay vì gọi ctx.switchToHttp().getRequest() lặp đi lặp lại.

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);

Sau đó, bạn có thể đơn giản dùng nó ở bất cứ đâu phù hợp với nhu cầu của mình.

@Get()
async findOne(@User() user: UserEntity) {
  console.log(user);
}
@Get()
@Bind(User())
async findOne(user) {
  console.log(user);
}

Truyền dữ liệu (Passing data)

Khi hành vi của decorator phụ thuộc vào một số điều kiện, bạn có thể dùng tham số data để truyền một argument vào factory function của decorator. Một use case cho việc này là một custom decorator trích xuất property từ object request theo key. Giả sử, ví dụ, tầng xác thực của chúng ta validate các request và gắn một user entity vào object request. User entity cho một request đã xác thực có thể trông như sau:

{
  "id": 101,
  "firstName": "Alan",
  "lastName": "Turing",
  "email": "[email protected]",
  "roles": ["admin"]
}

Hãy định nghĩa một decorator nhận vào tên của một property làm key, và trả về giá trị tương ứng nếu nó tồn tại (hoặc undefined nếu không tồn tại, hoặc nếu object user chưa được tạo).

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  (data: string, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const user = request.user;

    return data ? user?.[data] : user;
  },
);
import { createParamDecorator } from '@nestjs/common';

export const User = createParamDecorator((data, ctx) => {
  const request = ctx.switchToHttp().getRequest();
  const user = request.user;

  return data ? user && user[data] : user;
});

Đây là cách bạn có thể truy cập một property cụ thể thông qua decorator @User() trong controller:

@Get()
async findOne(@User('firstName') firstName: string) {
  console.log(`Hello ${firstName}`);
}
@Get()
@Bind(User('firstName'))
async findOne(firstName) {
  console.log(`Hello ${firstName}`);
}

Bạn có thể dùng cùng một decorator này với các key khác nhau để truy cập các property khác nhau. Nếu object user sâu hoặc phức tạp, điều này giúp việc triển khai request handler dễ đọc và dễ dàng hơn.

Ghi chú người dịch Tham số data ở đây đóng vai trò giống key bạn truyền vào context.Value(key) ở Go, hay argument thứ hai của một middleware factory ở Express (checkRole('admin')) - một giá trị tĩnh được “đóng” (closure) vào lúc bạn gọi decorator trong controller, khác với ctx (ExecutionContext) là thứ chỉ có ở runtime khi request thực sự tới.

Gợi ý Đối với người dùng TypeScript, lưu ý rằng createParamDecorator<T>() là một generic. Điều này có nghĩa là bạn có thể ép kiểu tường minh, ví dụ createParamDecorator<string>((data, ctx) => ...). Ngoài ra, bạn cũng có thể chỉ định type cho tham số trong factory function, ví dụ createParamDecorator((data: string, ctx) => ...). Nếu bỏ qua cả hai, type của data sẽ là any.

Làm việc với pipe (Working with pipes)

Nest xử lý custom param decorator theo cùng cách với các decorator có sẵn (@Body(), @Param() và @Query()). Điều này có nghĩa là pipe cũng được thực thi cho các tham số được đánh dấu tùy chỉnh (trong ví dụ của chúng ta, là tham số user). Hơn nữa, bạn có thể áp dụng pipe trực tiếp cho custom decorator:

@Get()
async findOne(
  @User(new ValidationPipe({ validateCustomDecorators: true }))
  user: UserEntity,
) {
  console.log(user);
}
@Get()
@Bind(User(new ValidationPipe({ validateCustomDecorators: true })))
async findOne(user) {
  console.log(user);
}

Gợi ý Lưu ý rằng option validateCustomDecorators phải được đặt thành true. ValidationPipe mặc định không validate các argument được đánh dấu bằng custom decorator.

Quy tắc tương tự cũng áp dụng cho StandardSchemaValidationPipe. Nếu custom decorator của bạn gắn dữ liệu cần được validate thông qua một schema tương thích Standard Schema, hãy bật validateCustomDecorators khi cấu hình pipe.

Kết hợp decorator (Decorator composition)

Nest cung cấp một helper method để kết hợp nhiều decorator lại với nhau. Ví dụ, giả sử bạn muốn gộp tất cả decorator liên quan đến xác thực thành một decorator duy nhất. Điều này có thể thực hiện với cấu trúc sau:

import { applyDecorators } from '@nestjs/common';

export function Auth(...roles: Role[]) {
  return applyDecorators(
    SetMetadata('roles', roles),
    UseGuards(AuthGuard, RolesGuard),
    ApiBearerAuth(),
    ApiUnauthorizedResponse({ description: 'Unauthorized' }),
  );
}
import { applyDecorators } from '@nestjs/common';

export function Auth(...roles) {
  return applyDecorators(
    SetMetadata('roles', roles),
    UseGuards(AuthGuard, RolesGuard),
    ApiBearerAuth(),
    ApiUnauthorizedResponse({ description: 'Unauthorized' }),
  );
}

Sau đó bạn có thể dùng custom decorator @Auth() này như sau:

@Get('users')
@Auth('admin')
findAllUsers() {}

Điều này có tác dụng áp dụng cả bốn decorator chỉ với một khai báo duy nhất.

Ghi chú người dịch applyDecorators không tạo ra hành vi mới, nó chỉ gọi lần lượt các decorator được truyền vào lên cùng một target - tương đương việc bạn stack nhiều middleware Express trong một hàm helper (app.use(cors(), helmet(), logger)) rồi export ra dùng chung. Vì vậy thứ tự các decorator bên trong applyDecorators vẫn quan trọng như khi bạn viết tay từng cái riêng lẻ.

Cảnh báo Decorator @ApiHideProperty() từ package @nestjs/swagger không thể kết hợp (composable) và sẽ không hoạt động đúng với hàm applyDecorators.

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.