☰ 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)

CLI Plugin

Bản dịch từ docs.nestjs.com/openapi/cli-plugin

Hệ thống reflection metadata của TypeScript có một số hạn chế khiến việc xác định một class gồm những property nào, hoặc nhận biết một property là optional hay required, trở nên không thể thực hiện được. Tuy nhiên, một số hạn chế này có thể được giải quyết tại thời điểm compile. Nest cung cấp một plugin giúp tăng cường quá trình compile TypeScript để giảm lượng code khung (boilerplate) cần viết.

Gợi ý Plugin này là tùy chọn (opt-in). Nếu muốn, bạn vẫn có thể khai báo tất cả decorator thủ công, hoặc chỉ khai báo riêng những decorator cần thiết.

Overview (tổng quan)

Swagger plugin sẽ tự động:

  • gắn @ApiProperty cho tất cả property của DTO trừ khi dùng @ApiHideProperty
  • đặt property required dựa trên dấu hỏi (question mark) (ví dụ: name?: string sẽ đặt required: false)
  • đặt property type hoặc enum dựa trên type (hỗ trợ cả array)
  • đặt property default dựa trên giá trị mặc định đã gán
  • đặt một số rule validation dựa trên decorator của class-validator (nếu classValidatorShim được đặt là true)
  • thêm response decorator cho mọi endpoint với status và type (response model) phù hợp
  • sinh mô tả (description) cho property và endpoint dựa trên comment (nếu introspectComments được đặt là true)
  • sinh giá trị ví dụ (example) cho property dựa trên comment (nếu introspectComments được đặt là true)

Lưu ý rằng tên file của bạn phải có một trong các hậu tố sau: ['.dto.ts', '.entity.ts'] (ví dụ: create-user.dto.ts) để được plugin phân tích.

Nếu bạn dùng hậu tố khác, bạn có thể điều chỉnh hành vi của plugin bằng option dtoFileNameSuffix (xem bên dưới).

Trước đây, nếu muốn có trải nghiệm tương tác với Swagger UI, bạn phải lặp lại rất nhiều code để cho package biết model/component của bạn nên được khai báo trong spec như thế nào. Ví dụ, bạn có thể định nghĩa một class CreateUserDto đơn giản như sau:

export class CreateUserDto {
  @ApiProperty()
  email: string;

  @ApiProperty()
  password: string;

  @ApiProperty({ enum: RoleEnum, default: [], isArray: true })
  roles: RoleEnum[] = [];

  @ApiProperty({ required: false, default: true })
  isEnabled?: boolean = true;
}

Với các project quy mô vừa, đây chưa phải vấn đề lớn, nhưng nó sẽ trở nên dài dòng và khó duy trì khi bạn có một số lượng lớn class.

Bằng cách bật Swagger plugin, class định nghĩa ở trên có thể được viết đơn giản như sau:

export class CreateUserDto {
  email: string;
  password: string;
  roles: RoleEnum[] = [];
  isEnabled?: boolean = true;
}

Lưu ý Swagger plugin sẽ suy ra annotation @ApiProperty() từ type TypeScript và decorator của class-validator. Việc này giúp mô tả API của bạn rõ ràng hơn cho tài liệu Swagger UI được sinh ra. Tuy nhiên, việc validate ở runtime vẫn do decorator của class-validator đảm nhiệm. Vì vậy, bạn vẫn cần tiếp tục dùng các validator như IsEmail(), IsNumber(),…

Do đó, nếu bạn muốn dựa vào annotation tự động để sinh tài liệu và vẫn muốn có validation ở runtime, thì decorator của class-validator vẫn là bắt buộc.

Gợi ý Khi dùng mapped types utilities (như PartialType) trong DTO, hãy import chúng từ @nestjs/swagger thay vì @nestjs/mapped-types để plugin có thể nhận diện được schema.

Plugin sẽ tự thêm các decorator phù hợp dựa trên Abstract Syntax Tree (cây cú pháp trừu tượng). Vì vậy bạn không cần phải vật lộn với các decorator @ApiProperty rải khắp code.

Gợi ý Plugin sẽ tự động sinh các property swagger còn thiếu, nhưng nếu bạn cần override, chỉ cần khai báo chúng một cách rõ ràng qua @ApiProperty().

Comments introspection (phân tích comment)

Khi bật tính năng phân tích comment (comments introspection), CLI plugin sẽ sinh description và example value cho property dựa trên comment.

Ví dụ, với property roles sau:

/**
 * A list of user's roles
 * @example ['admin']
 */
@ApiProperty({
  description: `A list of user's roles`,
  example: ['admin'],
})
roles: RoleEnum[] = [];

Bạn phải viết lặp lại cả description và example. Khi bật introspectComments, CLI plugin có thể trích xuất các comment này và tự động cung cấp description (và example, nếu có) cho property. Bây giờ, property trên có thể được khai báo đơn giản như sau:

/**
 * A list of user's roles
 * @example ['admin']
 */
roles: RoleEnum[] = [];

Có hai option plugin là dtoKeyOfComment và controllerKeyOfComment dùng để tùy biến cách plugin gán giá trị cho decorator ApiProperty và ApiOperation. Xem ví dụ dưới đây:

export class SomeController {
  /**
   * Create some resource
   */
  @Post()
  create() {}
}

Điều này tương đương với chỉ thị sau:

@ApiOperation({ summary: "Create some resource" })

Gợi ý Với model, logic tương tự cũng áp dụng, nhưng dùng với decorator ApiProperty thay vì ApiOperation.

Với controller, bạn không chỉ cung cấp được summary mà còn có thể cung cấp description (remarks), tag (như @deprecated), và response example, như sau:

/**
 * Create a new cat
 *
 * @remarks This operation allows you to create a new cat.
 *
 * @deprecated
 * @throws {500} Something went wrong.
 * @throws {400} Bad Request.
 */
@Post()
async create(): Promise<Cat> {}

Từ @nestjs/swagger v12, tag @param cũng được nhận diện, nhờ đó description của route parameter có thể lấy từ cùng một doc comment:

/**
 * List cats
 *
 * @param breed Filter results by breed
 * @param limit Maximum number of results to return
 */
@Get()
findAll(@Query('breed') breed?: string, @Query('limit') limit?: number) {}

Plugin sẽ thêm description vào @ApiQuery() mà nó đã tự sinh cho các tham số @Query() optional, sinh @ApiQuery({ name, description &#125;) cho các query parameter bắt buộc đã được viết doc comment, và sinh @ApiParam({ name, description &#125;) cho các tham số @Param() đã được viết doc comment.

Description được khớp theo tên biến (variable name), đây cũng chính là thứ mà @param mô tả — vì vậy @Query('order_by') orderBy: string sẽ được tài liệu hóa dưới dạng @param orderBy ..., không phải @param order_by .... Các decorator @ApiQuery() / @ApiParam() khai báo rõ ràng luôn được ưu tiên hơn và không bao giờ bị ghi đè.

Ghi chú người dịch Cơ chế “comments introspection” này giống với cách một số tool trong Go (như swaggo/swag) đọc comment dạng // @Param name query string true "..." phía trên hàm handler để sinh Swagger spec — Nest chỉ làm việc này “tự nhiên” hơn bằng cách đọc JSDoc chuẩn thay vì cú pháp comment riêng biệt. Điểm cần lưu ý: cơ chế này chỉ hoạt động khi bạn compile qua Nest CLI (hoặc tích hợp thủ công với ts-loader/ts-jest như phần dưới) — nếu bạn dùng tsc thuần hoặc một bundler khác không load plugin, các decorator sẽ không được tự sinh và code sẽ chạy nhưng thiếu tài liệu Swagger, dễ gây nhầm lẫn khi debug.

Using the CLI plugin (dùng CLI plugin)

Để bật plugin, mở nest-cli.json (nếu bạn dùng Nest CLI) và thêm cấu hình plugins sau:

{
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "plugins": ["@nestjs/swagger"]
  }
}

Bạn có thể dùng property options để tùy biến hành vi của plugin.

{
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "plugins": [
      {
        "name": "@nestjs/swagger",
        "options": {
          "classValidatorShim": false,
          "introspectComments": true,
          "skipAutoHttpCode": true
        }
      }
    ]
  }
}

Property options phải tuân theo interface sau:

export interface PluginOptions {
  dtoFileNameSuffix?: string[];
  controllerFileNameSuffix?: string[];
  classValidatorShim?: boolean;
  dtoKeyOfComment?: string;
  controllerKeyOfComment?: string;
  introspectComments?: boolean;
  skipAutoHttpCode?: boolean;
  esmCompatible?: boolean;
}
Option Giá trị mặc định Mô tả
dtoFileNameSuffix ['.dto.ts', '.entity.ts'] Hậu tố file DTO (Data Transfer Object)
controllerFileNameSuffix .controller.ts Hậu tố file controller
classValidatorShim true Nếu đặt là true, module sẽ tái sử dụng các decorator validation của class-validator (ví dụ @Max(10) sẽ thêm max: 10 vào định nghĩa schema)
dtoKeyOfComment 'description' Property key dùng để gán nội dung comment vào ApiProperty.
controllerKeyOfComment 'summary' Property key dùng để gán nội dung comment vào ApiOperation.
introspectComments false Nếu đặt là true, plugin sẽ sinh description và example value cho property dựa trên comment
skipAutoHttpCode false Vô hiệu hóa việc tự động thêm @HttpCode() trong controller
esmCompatible false Nếu đặt là true, khắc phục lỗi cú pháp gặp phải khi dùng ESM ({ "type": "module" }).

Nhớ xóa thư mục /dist và rebuild lại ứng dụng mỗi khi cập nhật option của plugin. Nếu bạn không dùng CLI mà thay vào đó dùng cấu hình webpack tùy chỉnh, bạn có thể dùng plugin này kết hợp với ts-loader:

getCustomTransformers: (program: any) => ({
  before: [require('@nestjs/swagger/plugin').before({}, program)]
}),

SWC builder

Với các setup thông thường (không phải monorepo), để dùng CLI Plugin với SWC builder, bạn cần bật type checking, như mô tả ở đây.

$ nest start -b swc --type-check

Với setup monorepo, làm theo hướng dẫn ở đây.

$ npx ts-node src/generate-metadata.ts
# hoặc npx ts-node apps/{YOUR_APP}/src/generate-metadata.ts

Bây giờ, file metadata đã serialize phải được load bằng method SwaggerModule#loadPluginMetadata, như bên dưới:

import metadata from './metadata.js'; // <-- file được tự sinh bởi "PluginMetadataGenerator"

await SwaggerModule.loadPluginMetadata(metadata); // <-- ở đây
const document = SwaggerModule.createDocument(app, config);

Ghi chú người dịch SWC (Speedy Web Compiler) là một compiler thay thế tsc, viết bằng Rust, được Nest CLI hỗ trợ để build nhanh hơn nhiều so với compiler TypeScript mặc định. Cái giá phải trả là SWC không tự chạy type-checking đầy đủ và không thể tự chạy AST transformer (như CLI plugin của swagger) trong lúc build — nên Nest phải sinh metadata ra một file riêng (generate-metadata.ts) từ trước, rồi load thủ công lúc runtime. Nếu bạn đổi DTO mà quên chạy lại bước generate-metadata này, Swagger UI sẽ hiển thị schema cũ mà không báo lỗi gì — đây là một cạm bẫy thực tế khá dễ gặp khi chuyển từ tsc sang SWC.

Integration with ts-jest (e2e tests) — tích hợp với ts-jest (test e2e)

Để chạy e2e test, ts-jest compile source code của bạn ngay tại thời điểm chạy (on the fly), trong memory. Điều này có nghĩa là nó không dùng Nest CLI compiler và không áp dụng bất kỳ plugin hay AST transformation nào.

Để bật plugin, tạo file sau trong thư mục e2e test của bạn:

const transformer = require('@nestjs/swagger/plugin');

module.exports.name = 'nestjs-swagger-transformer';
// bạn nên đổi số version này mỗi khi thay đổi cấu hình bên dưới - nếu không jest sẽ không phát hiện được thay đổi
module.exports.version = 1;

module.exports.factory = (cs) => {
  return transformer.before(
    {
      // options của @nestjs/swagger/plugin (có thể để trống)
    },
    cs.program, // "cs.tsCompiler.program" cho các phiên bản Jest cũ hơn (<= v27)
  );
};

Sau khi thiết lập xong, import AST transformer trong file cấu hình jest của bạn. Theo mặc định (trong ứng dụng starter), file cấu hình e2e test nằm trong thư mục test và tên là jest-e2e.json.

Nếu bạn dùng jest@<29, hãy dùng đoạn code dưới đây.

{
  ... // các cấu hình khác
  "globals": {
    "ts-jest": {
      "astTransformers": {
        "before": ["<đường dẫn đến file đã tạo ở trên>"]
      }
    }
  }
}

Nếu bạn dùng jest@^29, hãy dùng đoạn code dưới đây, vì cách làm trước đó đã bị deprecated.

{
  ... // các cấu hình khác
  "transform": {
    "^.+\\.(t|j)s$": [
      "ts-jest",
      {
        "astTransformers": {
          "before": ["<đường dẫn đến file đã tạo ở trên>"]
        }
      }
    ]
  }
}

Troubleshooting jest (e2e tests) — xử lý sự cố jest (test e2e)

Trong trường hợp jest không nhận ra các thay đổi cấu hình của bạn, có thể do Jest đã cache kết quả build trước đó. Để áp dụng cấu hình mới, bạn cần xóa thư mục cache của Jest.

Để xóa thư mục cache, chạy lệnh sau trong thư mục project NestJS của bạn:

$ npx jest --clearCache

Trong trường hợp việc xóa cache tự động không thành công, bạn vẫn có thể xóa thư mục cache bằng tay với các lệnh sau:

# Tìm thư mục cache của jest (thường là /tmp/jest_rs)
# bằng cách chạy lệnh sau trong thư mục gốc project NestJS
$ npx jest --showConfig | grep cache
# ví dụ kết quả:
#   "cache": true,
#   "cacheDirectory": "/tmp/jest_rs"

# Xóa hoặc làm trống thư mục cache của Jest
$ rm -rf  <giá trị cacheDirectory>
# ví dụ:
# rm -rf /tmp/jest_rs

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.