☰ 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ữ
OpenAPI (Swagger)

Operations (thao tác)

Bản dịch từ docs.nestjs.com/openapi/operations

Trong thuật ngữ OpenAPI, path là các endpoint (resource) như /users hay /reports/summary mà API của bạn expose, còn operation là các HTTP method dùng để thao tác trên các path đó, ví dụ GET, POST hay DELETE.

Tag

Để gắn một controller vào một tag cụ thể, dùng decorator @ApiTags(...tags).

@ApiTags('cats')
@Controller('cats')
export class CatsController {}

OpenAPI 3.2 mở rộng Tag Object để tag có thể được tổ chức theo dạng phân cấp (hierarchy) và gắn kèm một gợi ý (hint) về cách chúng nên được hiển thị. Để khai báo các quan hệ này, định nghĩa tag trước bằng DocumentBuilder và truyền các tùy chọn parent và kind cho addTag():

const config = new DocumentBuilder()
  .setOpenAPIVersion('3.2.0')
  .addTag('Animals', 'Everything about animals', undefined, { kind: 'nav' })
  .addTag('Cats', 'Cat operations', undefined, { parent: 'Animals' })
  .addTag('Dogs', 'Dog operations', undefined, { parent: 'Animals' })
  .build();

Tùy chọn parent tham chiếu đến một tag khác theo tên, còn kind là một chuỗi tự do, dùng để máy đọc được (machine-readable), gợi ý cách tag nên được sử dụng — thường là nav, badge, hoặc audience.

Cảnh báo Các field parent và kind thuộc về Tag Object của OpenAPI 3.2. Bạn phải gọi setOpenAPIVersion('3.2.0'), nếu không document được tạo ra vẫn khai báo openapi: 3.0.0 và các validator nghiêm ngặt sẽ từ chối các field này. Các field phân cấp này chỉ có thể được định nghĩa qua DocumentBuilder.addTag(); thiết lập chúng trên decorator @ApiTags() không có tác dụng gì.

Ghi chú người dịch Nếu bạn từng làm việc với OpenAPI/Swagger từ trước, đừng nhầm đây với tính năng “nhóm route bằng prefix” kiểu router group của Laravel hay Express. Đây thuần là metadata mô tả — nó không ảnh hưởng đến việc route thật của bạn được match hay xử lý thế nào, chỉ ảnh hưởng đến cách Swagger UI hiển thị và sắp xếp danh sách endpoint (ví dụ nhóm “Cats” và “Dogs” lồng trong “Animals” trên UI).

Để định nghĩa các header tùy chỉnh được mong đợi là một phần của request, dùng @ApiHeader().

@ApiHeader({
  name: 'X-MyHeader',
  description: 'Custom header',
})
@Controller('cats')
export class CatsController {}

Response

Để định nghĩa một HTTP response tùy chỉnh, dùng decorator @ApiResponse().

@Post()
@ApiResponse({ status: 201, description: 'The record has been successfully created.'})
@ApiResponse({ status: 403, description: 'Forbidden.'})
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

Nest cung cấp một tập decorator API response viết tắt, kế thừa từ decorator @ApiResponse:

  • @ApiOkResponse()
  • @ApiCreatedResponse()
  • @ApiAcceptedResponse()
  • @ApiNoContentResponse()
  • @ApiMovedPermanentlyResponse()
  • @ApiFoundResponse()
  • @ApiBadRequestResponse()
  • @ApiUnauthorizedResponse()
  • @ApiNotFoundResponse()
  • @ApiForbiddenResponse()
  • @ApiMethodNotAllowedResponse()
  • @ApiNotAcceptableResponse()
  • @ApiRequestTimeoutResponse()
  • @ApiConflictResponse()
  • @ApiPreconditionFailedResponse()
  • @ApiTooManyRequestsResponse()
  • @ApiGoneResponse()
  • @ApiPayloadTooLargeResponse()
  • @ApiUnsupportedMediaTypeResponse()
  • @ApiUnprocessableEntityResponse()
  • @ApiInternalServerErrorResponse()
  • @ApiNotImplementedResponse()
  • @ApiBadGatewayResponse()
  • @ApiServiceUnavailableResponse()
  • @ApiGatewayTimeoutResponse()
  • @ApiDefaultResponse()
@Post()
@ApiCreatedResponse({ description: 'The record has been successfully created.'})
@ApiForbiddenResponse({ description: 'Forbidden.'})
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

Để chỉ định một model trả về (return model) cho một request, chúng ta phải tạo một class và annotate tất cả property bằng decorator @ApiProperty().

export class Cat {
  @ApiProperty()
  id: number;

  @ApiProperty()
  name: string;

  @ApiProperty()
  age: number;

  @ApiProperty()
  breed: string;
}

Sau đó, model Cat có thể được dùng kết hợp với property type của response decorator.

@ApiTags('cats')
@Controller('cats')
export class CatsController {
  @Post()
  @ApiCreatedResponse({
    description: 'The record has been successfully created.',
    type: Cat,
  })
  async create(@Body() createCatDto: CreateCatDto): Promise<Cat> {
    return this.catsService.create(createCatDto);
  }
}

Hãy mở browser để kiểm tra model Cat được tạo ra:

Minh hoạ: swagger response type

Thay vì định nghĩa response cho từng endpoint hoặc controller riêng lẻ, bạn có thể định nghĩa một response toàn cục (global) cho tất cả endpoint bằng class DocumentBuilder. Cách này hữu ích khi bạn muốn định nghĩa một response chung cho toàn bộ endpoint trong ứng dụng (ví dụ, cho các lỗi như 401 Unauthorized hay 500 Internal Server Error).

const config = new DocumentBuilder()
  .addGlobalResponse({
    status: 500,
    description: 'Internal server error',
  })
  // other configurations
  .build();

Upload file

Bạn có thể bật upload file cho một method cụ thể bằng decorator @ApiBody kết hợp với @ApiConsumes(). Đây là một ví dụ đầy đủ, dùng kỹ thuật Upload file:

@UseInterceptors(FileInterceptor('file'))
@ApiConsumes('multipart/form-data')
@ApiBody({
  description: 'List of cats',
  type: FileUploadDto,
})
uploadFile(@UploadedFile() file: Express.Multer.File) {}

Trong đó FileUploadDto được định nghĩa như sau:

class FileUploadDto {
  @ApiProperty({ type: 'string', format: 'binary' })
  file: any;
}

Để xử lý upload nhiều file, bạn có thể định nghĩa FilesUploadDto như sau:

class FilesUploadDto {
  @ApiProperty({ type: 'array', items: { type: 'string', format: 'binary' } })
  files: any[];
}

Extension

Để thêm một Extension vào một request, dùng decorator @ApiExtension(). Tên extension phải có tiền tố x-.

@ApiExtension('x-foo', { hello: 'world' })

Nâng cao: ApiResponse dạng generic

Với khả năng cung cấp Định nghĩa thô, chúng ta có thể định nghĩa schema Generic cho Swagger UI. Giả sử chúng ta có DTO sau:

export class PaginatedDto<TData> {
  @ApiProperty()
  total: number;

  @ApiProperty()
  limit: number;

  @ApiProperty()
  offset: number;

  results: TData[];
}

Chúng ta bỏ qua việc annotate results vì sẽ cung cấp một định nghĩa thô cho nó sau. Bây giờ, hãy định nghĩa một DTO khác và đặt tên là, chẳng hạn, CatDto, như sau:

export class CatDto {
  @ApiProperty()
  name: string;

  @ApiProperty()
  age: number;

  @ApiProperty()
  breed: string;
}

Với những gì đã có, chúng ta có thể định nghĩa một response PaginatedDto<CatDto>, như sau:

@ApiOkResponse({
  schema: {
    allOf: [
      { $ref: getSchemaPath(PaginatedDto) },
      {
        properties: {
          results: {
            type: 'array',
            items: { $ref: getSchemaPath(CatDto) },
          },
        },
      },
    ],
  },
})
async findAll(): Promise<PaginatedDto<CatDto>> {}

Trong ví dụ này, chúng ta chỉ định rằng response sẽ có allOf PaginatedDto và property results sẽ có type Array<CatDto>.

  • Function getSchemaPath() trả về đường dẫn Schema OpenAPI từ trong file đặc tả OpenAPI cho một model cho trước.
  • allOf là một khái niệm mà OAS 3 cung cấp để xử lý các use-case liên quan đến kế thừa (inheritance).

Cuối cùng, vì PaginatedDto không được tham chiếu trực tiếp bởi bất kỳ controller nào, SwaggerModule sẽ chưa thể tạo được định nghĩa model tương ứng. Trong trường hợp này, chúng ta phải thêm nó như một Model bổ sung. Ví dụ, chúng ta có thể dùng decorator @ApiExtraModels() ở cấp controller, như sau:

@Controller('cats')
@ApiExtraModels(PaginatedDto)
export class CatsController {}

Nếu bạn chạy Swagger ngay bây giờ, file swagger.json được tạo ra cho endpoint cụ thể này sẽ có response được định nghĩa như sau:

"responses": {
  "200": {
    "description": "",
    "content": {
      "application/json": {
        "schema": {
          "allOf": [
            {
              "$ref": "#/components/schemas/PaginatedDto"
            },
            {
              "properties": {
                "results": {
                  "$ref": "#/components/schemas/CatDto"
                }
              }
            }
          ]
        }
      }
    }
  }
}

Để làm cho nó tái sử dụng được, chúng ta có thể tạo một decorator tùy chỉnh cho PaginatedDto, như sau:

export const ApiPaginatedResponse = <TModel extends Type<any>>(
  model: TModel,
) => {
  return applyDecorators(
    ApiExtraModels(PaginatedDto, model),
    ApiOkResponse({
      schema: {
        allOf: [
          { $ref: getSchemaPath(PaginatedDto) },
          {
            properties: {
              results: {
                type: 'array',
                items: { $ref: getSchemaPath(model) },
              },
            },
          },
        ],
      },
    }),
  );
};

Gợi ý Interface Type<any> và function applyDecorators được import từ package @nestjs/common.

Để đảm bảo SwaggerModule sẽ tạo một định nghĩa cho model của chúng ta, chúng ta phải thêm nó như một model bổ sung, giống như đã làm với PaginatedDto ở controller trước đó.

Với những gì đã có, chúng ta có thể dùng decorator tùy chỉnh @ApiPaginatedResponse() trên endpoint của mình:

@ApiPaginatedResponse(CatDto)
async findAll(): Promise<PaginatedDto<CatDto>> {}

Đối với các công cụ sinh code client, cách làm này gây ra sự mơ hồ (ambiguity) trong việc PaginatedResponse<TModel> được sinh ra cho client như thế nào. Đoạn code sau là ví dụ về kết quả của một công cụ sinh code client cho endpoint GET / ở trên.

// Angular
findAll(): Observable<{ total: number, limit: number, offset: number, results: CatDto[] }>

Như bạn thấy, kiểu trả về (Return Type) ở đây bị mơ hồ. Để giải quyết vấn đề này, bạn có thể thêm property title vào schema cho ApiPaginatedResponse:

export const ApiPaginatedResponse = <TModel extends Type<any>>(
  model: TModel,
) => {
  return applyDecorators(
    ApiOkResponse({
      schema: {
        title: `PaginatedResponseOf${model.name}`,
        allOf: [
          // ...
        ],
      },
    }),
  );
};

Giờ đây kết quả của công cụ sinh code client sẽ trở thành:

// Angular
findAll(): Observable<PaginatedResponseOfCatDto>

Ghi chú người dịch Nếu bạn từng viết OpenAPI tay ở PHP/Laravel (ví dụ qua l5-swagger), phần allOf này tương đương với việc bạn merge (gộp) hai schema lại: một schema base (PaginatedDto) và một schema override chỉ định lại field results. Vấn đề “Return Type mơ hồ” ở đây không phải do Nest hay Swagger có lỗi, mà do bản thân OpenAPI 3.0 không có khái niệm generic thật — allOf chỉ là cách xấp xỉ (approximate) generic bằng cách kết hợp schema tĩnh, nên công cụ sinh code client không tự suy ra được kiểu PaginatedDto<CatDto> nếu bạn không đặt title để đánh dấu rõ nó là một biến thể riêng.

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.