Tự động tạo tài liệu API bằng OpenAPI & Scalar

Tài liệu API (API Documentation) là thành phần không thể thiếu khi phát triển Backend. Nó giúp các lập trình viên Frontend, Mobile hoặc đối tác tích hợp hiểu rõ cách tương tác với hệ thống của bạn mà không cần đọc trực tiếp mã nguồn. Thay vì viết tài liệu thủ công hoặc sử dụng giao diện Swagger UI truyền thống đã lỗi thời, chúng ta sẽ kết hợp OpenAPI (Swagger) của NestJS với Scalar – một giao diện tài liệu API hiện đại, trực quan, hỗ trợ thử nghiệm trực tiếp (Request Client) và tự động sinh mã code tích hợp cho nhiều ngôn ngữ khác nhau.

1. Cài đặt thư viện cần thiết

Để bắt đầu, bạn cần cài đặt bộ thư viện tạo OpenAPI của NestJS cùng với middleware giao diện của Scalar:
npm install @nestjs/swagger @scalar/nestjs-api-reference

2. Cấu hình tài liệu API trong tệp khởi động

Sau khi cài đặt xong, bạn hãy cấu hình tài liệu API trong tệp khởi động src/main.ts của ứng dụng:
src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { apiReference } from '@scalar/nestjs-api-reference';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 1. Cấu hình đặc tả OpenAPI (Swagger)
  const config = new DocumentBuilder()
    .setTitle('Hệ thống Quản lý Dự án API')
    .setDescription('Tài liệu API chi tiết cho ứng dụng quản lý tác vụ và dự án.')
    .setVersion('1.0')
    .addBearerAuth() // Bật hỗ trợ xác thực JWT Token (nếu có)
    .build();

  const document = SwaggerModule.createDocument(app, config);

  // 2. Tích hợp giao diện tài liệu Scalar vào endpoint /reference
  app.use(
    '/reference',
    apiReference({
      theme: 'purple', // Các chủ đề: 'default', 'purple', 'moon', 'solarized', 'blue'
      spec: {
        content: document,
      },
    }),
  );

  await app.listen(3000);
  console.log('Tài liệu API đang chạy tại: http://localhost:3000/reference');
}
bootstrap();

3. Các tình huống gán Decorator phổ biến trong dự án

Để đặc tả chi tiết các trường thông tin trong DTO và các endpoint trong Controller, NestJS cung cấp một bộ các decorator tương ứng theo từng tình huống nghiệp vụ:

A. Tình huống mô tả thuộc tính trong DTO

Khi Client gửi dữ liệu lên, bạn cần mô tả rõ kiểu dữ liệu, ý nghĩa và ví dụ mẫu của từng trường thông tin.
  • @ApiProperty(): Khai báo thuộc tính bắt buộc.
  • @ApiPropertyOptional(): Khai báo thuộc tính không bắt buộc (tương đương @ApiProperty({ required: false })).

Các tham số cấu hình chính trong @ApiProperty:

Tham sốÝ nghĩaVí dụ
descriptionMô tả ý nghĩa của trường dữ liệu.description: 'Tên đăng nhập'
exampleVí dụ dữ liệu thực tế giúp sinh tài liệu trực quan.example: 'john_doe'
typeKiểu dữ liệu (nếu là đối tượng lồng nhau hoặc không tự nhận diện).type: () => UserProfile
enumDanh sách các giá trị được chấp nhận.enum: ['ACTIVE', 'INACTIVE']
isArrayXác định trường này là một mảng.isArray: true
src/users/dto/create-user-profile.dto.ts
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

export enum UserRole {
  ADMIN = 'ADMIN',
  USER = 'USER',
}

export class CreateUserProfileDto {
  @ApiProperty({
    description: 'Họ và tên đầy đủ',
    example: 'Nguyễn Văn A',
  })
  fullName: string;

  @ApiPropertyOptional({
    description: 'Số điện thoại liên hệ',
    example: '0987654321',
  })
  phoneNumber?: string;

  @ApiProperty({
    description: 'Vai trò của tài khoản',
    enum: UserRole,
    example: UserRole.USER,
  })
  role: UserRole;

  @ApiProperty({
    description: 'Danh sách các kỹ năng',
    type: [String],
    example: ['NestJS', 'TypeScript'],
  })
  skills: string[];
}

B. Tình huống gom nhóm và đặt tên Endpoint (Controller)

  • @ApiTags('tên_nhóm'): Gom nhóm các API liên quan vào một thư mục trên giao diện tài liệu giúp dễ tìm kiếm (thường đặt ở cấp Class của Controller).
  • @ApiOperation({ summary, description }): Mô tả ngắn gọn tính năng của endpoint (summary) và ghi chú chi tiết logic xử lý bên trong nếu cần (description).
src/users/users.controller.ts
import { Controller } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';

@ApiTags('Người dùng (Users)') // Gom nhóm toàn bộ endpoint trong controller này
@Controller('users')
export class UsersController {
  // ...
}

C. Tình huống nhận tham số từ URL (Route Params & Query String)

Khi endpoint cần nhận các tham số lọc hoặc tìm kiếm, bạn cần ghi chú rõ ràng để kiểm thử trực tiếp trên giao diện.
  • @ApiParam(): Đặc tả tham số động trên URL đường dẫn (ví dụ: :id, :code).
  • @ApiQuery(): Đặc tả các tham số lọc gửi sau dấu chấm hỏi (ví dụ: ?page=1&limit=10).
  @Get(':id')
  @ApiOperation({ summary: 'Lấy thông tin người dùng bằng ID' })
  @ApiParam({
    name: 'id',
    description: 'Mã định danh duy nhất của người dùng',
    type: String,
    example: 'usr_123456',
  })
  findOne(@Param('id') id: string) {
    return this.usersService.findOne(id);
  }

  @Get()
  @ApiOperation({ summary: 'Lấy danh sách người dùng phân trang' })
  @ApiQuery({
    name: 'page',
    description: 'Số trang dữ liệu cần lấy',
    required: false,
    type: Number,
    example: 1,
  })
  @ApiQuery({
    name: 'limit',
    description: 'Số lượng bản ghi trên mỗi trang',
    required: false,
    type: Number,
    example: 10,
  })
  findAll(@Query('page') page = 1, @Query('limit') limit = 10) {
    return this.usersService.findAll(page, limit);
  }

D. Tình huống định nghĩa kết quả trả về (Responses)

Ghi chú đầy đủ các mã trạng thái phản hồi HTTP giúp phía Client xử lý chính xác các trường hợp thành công hoặc lỗi:
  • @ApiResponse(): Đặc tả phản hồi tổng quát bằng cách chỉ định mã số status.
  • Các decorator chuyên biệt (khuyên dùng):
    • @ApiOkResponse({ description, type }): Phản hồi thành công với mã 200 (thường cho GET, PUT, PATCH, DELETE).
    • @ApiCreatedResponse({ description, type }): Phản hồi thành công với mã 201 (thường cho POST).
    • @ApiBadRequestResponse({ description }): Lỗi dữ liệu đầu vào không hợp lệ (mã 400).
    • @ApiUnauthorizedResponse({ description }): Lỗi chưa xác thực thông tin đăng nhập (mã 401).
    • @ApiNotFoundResponse({ description }): Lỗi không tìm thấy tài nguyên (mã 404).
  @Post()
  @ApiOperation({ summary: 'Đăng ký tài khoản mới' })
  @ApiCreatedResponse({
    description: 'Đăng ký tài khoản thành công.',
    type: UserResponseDto, // Chỉ định DTO trả về để tự động vẽ cấu trúc dữ liệu kết quả
  })
  @ApiBadRequestResponse({ description: 'Email đã tồn tại hoặc dữ liệu không hợp lệ.' })
  register(@Body() dto: CreateUserDto) {
    return this.usersService.register(dto);
  }

E. Tình huống yêu cầu bảo mật (Xác thực JWT Token)

Nếu API yêu cầu phải gửi Token trong Header để truy cập, bạn cần đánh dấu bảo mật để giao diện Scalar hiển thị nút nhập token và tự động đính kèm vào các lượt gọi thử nghiệm.
  • @ApiBearerAuth(): Khai báo endpoint yêu cầu xác thực dạng Bearer Token (JWT).
    • Lưu ý: Phải gọi .addBearerAuth() trong main.ts trước.
  @Get('profile')
  @ApiBearerAuth() // Yêu cầu Client phải truyền Token qua Header để thực thi
  @ApiOperation({ summary: 'Lấy thông tin cá nhân của tài khoản hiện tại' })
  getProfile(@Request() req) {
    return req.user;
  }

F. Tình huống tải lên tập tin (File Upload)

Đối với các API tải ảnh hoặc tập tin (avatar, tài liệu), bạn cần định cấu hình kiểu truyền tải là multipart/form-data và ghi chú rõ trường nhận file.
  • @ApiConsumes('multipart/form-data'): Khai báo API nhận dữ liệu dạng form-data chứa tập tin.
  • @ApiBody(): Đặc tả trực tiếp cấu trúc body nhận tập tin nhị phân.
  @Post('upload-avatar')
  @ApiConsumes('multipart/form-data')
  @ApiOperation({ summary: 'Tải lên ảnh đại diện của người dùng' })
  @ApiBody({
    schema: {
      type: 'object',
      properties: {
        file: {
          type: 'string',
          format: 'binary',
          description: 'Tệp ảnh đại diện (PNG, JPG)',
        },
      },
    },
  })
  uploadAvatar(@UploadedFile() file: Express.Multer.File) {
    return { imageUrl: `/uploads/${file.filename}` };
  }

4. Cách phối hợp viết OpenAPI với Claude Code

Việc gán thủ công từng decorator cho tất cả DTO và Controller có thể tốn nhiều thời gian. Bạn có thể sử dụng Claude Code để tự động hóa hoàn toàn quy trình này thông qua các prompt hướng dẫn chi tiết.

Prompt mẫu bổ sung OpenAPI:

Tôi vừa cấu hình xong OpenAPI và Scalar cho dự án. Hãy giúp tôi bổ sung các decorator OpenAPI từ thư viện '@nestjs/swagger' vào 'TasksController' và 'CreateTaskDto' trong thư mục 'src/tasks':

1. Tại 'CreateTaskDto':
   - Sử dụng '@ApiProperty' cho tất cả các trường dữ liệu.
   - Thêm phần mô tả (description) bằng tiếng Việt và ví dụ (example) trực quan cho từng trường.

2. Tại 'TasksController':
   - Sử dụng '@ApiTags' để nhóm controller này vào mục 'Tác Vụ'.
   - Sử dụng '@ApiOperation' mô tả tóm tắt tính năng của từng hàm (ví dụ: Tạo tác vụ, Xóa tác vụ).
   - Sử dụng '@ApiResponse' cho các mã trạng thái thành công (200, 201) và lỗi (400, 404).
   - Sử dụng '@ApiParam' hoặc '@ApiQuery' nếu hàm đó có tham số trên URL hoặc query string.

5. Xem và kiểm thử API trên trình duyệt

  1. Khởi động ứng dụng NestJS của bạn:
    npm run start:dev
    
  2. Mở trình duyệt và truy cập đường dẫn: http://localhost:3000/reference
  3. Tại giao diện Scalar, bạn có thể:
    • Xem toàn bộ cấu trúc các DTO dưới dạng tài liệu mô tả chi tiết.
    • Click chọn mục Test Request để gửi trực tiếp yêu cầu HTTP đến Backend và kiểm tra kết quả phản hồi.
    • Chọn mục Code Snippets để sao chép mã nguồn gọi API tương ứng bằng JavaScript, Python, Go, Curl, v.v.