owned this note
owned this note
Published
Linked with GitHub
[TOC]
## 安裝依賴項
```npm!
npm install --save @nestjs/swagger
```
## Swagger Open API Version
- OAS 3.0
## Step 1:main.ts 引用 swagger plugin
[範例檔案](https://stackblitz.com/edit/nestjs-typescript-starter-wb9ii32e?file=src%2Fapp.controller.ts)
【 Swagger DocumentBuilder 方法使用說明 】
- setVersion:設定 API 文件標題
- setDescription:設定 API 文件描述
- setContact:設定 API 維護者的聯絡資訊
- addServer:設定 API 服務器環境資訊,通常包含正式及測試環境
- addTag:設定 API 的分類標籤
main.ts 檔案內容:
```javascript!
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ConfigService } from '@nestjs/config';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
const config = new DocumentBuilder()
.setTitle('Nest 練習使用') // 文件標題
.setDescription(
`注意事項:登入成功後請點「Authorize」輸入 Token。
範例程式碼:
\`\`\`javascript
const config = {
headers: { Authorization: token },
};
axios
.post('/V1/docs/heartbeat', {}, config)
.then((res) => {
console.log(res.data);
}).catch((error) => {
console.log(error.response.data);
});
\`\`\`
`,
)
.setVersion('1.0') // 版號
.setContact('Antonio', '', 'test123@gmail.com') // 聯絡資訊
.addServer(configService.get<string>('BASEURL'), '生產環境') // 讀取環境變數
.addServer('https://api.example.com', '測試環境') // 可以設置不同測試環境
.addTag('heartbeat', '心跳檢測') // 新增分類的 tag Name 及 description
.addTag('users', '用戶資訊') // 新增分類的 tag Name 及 description
.build();
const document = SwaggerModule.createDocument(app, config, {
// 關閉預設 Tag,預設會把所有掃到的 API Route 都加入 App 的 default tag 內
autoTagControllers: false,
});
const options = {
jsonDocumentUrl: 'swagger/json', // 啟用 swagger json 讀取 url
swaggerOptions: {
defaultModelsExpandDepth: 1, // 禁用 Models 展開
},
};
SwaggerModule.setup('api', app, document, options); // api 是 swagger UI使用路由
await app.listen(3000);
}
bootstrap();
```
:::info
啟動之後,可以透過以下路徑使用 Swagger:
1. [baseUrl]/api:讀取到 Swagger UI 介面
2. [baseUrl]/swagger/json:讀取到 Swagger json 完整文檔內容
:::
### 呈現畫面(swagger metadata 設置)

## Step 2:針對路由 Controller 設置分類標籤
### 情境一:GET Method + HttpStatus 204
【 Controller method decorator 使用說明 】
- @ApiTags:用於為路由方法或控制器設定分類標籤。
- @ApiOperation:為每個路由方法提供簡短的功能概述(summary)和詳細描述(description),用於說明該 API 的具體用途和行為。
- @ApiResponse:根據不同狀態碼,定義 API 的回應情況
app.controller.ts 檔案內容:
```javascript!
import {
Controller,
Header,
Post,
Get,
Redirect,
HttpRedirectResponse,
HttpCode,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
@Controller('docs')
export class AppController {
constructor() {}
@ApiTags('heartbeat')
// API 描述使用
@ApiOperation({
summary: '檢查服務健康狀態',
description: '回傳 204 狀態碼,表示服務正常運行。',
})
// 響應描述
@ApiResponse({
status: 204,
description: '服務正常運行,沒有內容返回。',
})
@Get('heartbeat')
@HttpCode(204)
getHeartbeat(): void {}
}
```
#### 呈現畫面(swagger UI GET 204 範例設置):

### 情境二:POST Method + HttpStatus 201(使用 DTO 映射 Swagger API Schema)
app.controller.ts 檔案內容:
```javascript!
import {
Controller,
Header,
Post,
Get,
Redirect,
HttpRedirectResponse,
HttpCode,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
@Controller('docs')
export class AppController {
constructor() {}
@Post()
@ApiTags('users')
@ApiOperation({
summary: '取得全部用戶資訊',
description: '返回系統中所有的用戶資訊,包括用戶名稱、角色和狀態。',
})
@ApiResponse({
status: 201,
description: 'User created successfully.',
type: CreateUserResponseDto,
})
createUserInfoPromise(
@Body() createUserDto: CreateUserDto,
): CreateUserResponseDto {
return { message: 'User created' };
}
}
```
:::info
@ApiResponse 的部分如果有需要指定狀態碼而有不同描述,可以選擇新增。預設 Post 會使用 201 狀態碼。不新增的情況下,會根據路由中使用的 ResponseDto 自己生成無描述的版本。
:::
dto/CreateUser.dto.ts 檔案內容:
```javascript!
import { IsNotEmpty, IsPositive, IsString, MaxLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CreateUserDto {
@ApiProperty({
description: "The user's name (must not exceed 10 characters)",
example: 'John Doe',
maxLength: 10,
})
@IsString()
@IsNotEmpty()
@MaxLength(10, { message: 'Name must not exceed 10 characters' })
name: string;
@ApiProperty({
description: "The user's age in years. Must be a positive number.",
example: 30,
})
@IsPositive()
age: number;
}
```
#### 呈現畫面(swagger UI POST 201):

**APIProperty 主要切到 schema** 可以看到參數說明

## 如果不想在 Swagger UI 上顯示 Schmas 可以這麼做
Swagger UI Schemas 區塊說明:

使用 Swagger 官方設定:[defaultModelsExpandDepth](https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/)
main.ts 檔案內容調整:
```javascript!
// ... 略
const options = {
swaggerOptions: {
defaultModelsExpandDepth: 1, // 禁用 Models 展開
},
};
SwaggerModule.setup('api', app, document, options);
```
## @nestjs/swagger plugin + JSDoc 簡化 Entity 標註 @ApiProperty 神奇魔法(非常重要)
[範例檔案](https://stackblitz.com/edit/nestjs-typescript-starter-bhwhc28z?file=src%2Fdto%2FCreateUserResponse.dto.ts)
### Step 1:安裝 @nestjs/swagger plugin
```npm!
npm i @nestjs/swagger
```
### Step 2:進入根目錄下的 nest-cli.json 進行設定
- classValidatorShim:啟用 class-validator 的驗證規則,用於補充屬性的驗證資訊
- introspectComments:啟用 JSDoc 註釋解析,用於產生 API 文檔
nest-cli.json 檔案內容如(專注在 plugins 部分)下:
```json!
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true,
"plugins": [
{
"name": "@nestjs/swagger",
"options": {
"classValidatorShim": true, // 啟用 class-validator 的驗證規則,用於補充屬性的驗證資訊
"introspectComments": true, // 啟用 JSDoc 註釋解析,用於產生 API 文檔
"debug": true
}
}
]
}
}
```
:::info
**注意事項**:
1. 預設只檢查`['.dto.ts', '.entity.ts']`副檔名
2. 只有在 Router 上有使用的 dto 及 entity 才會被`@nest/swagger`處理,單純匯入不會處理。
:::
### Step 3:改造原先的 DTO 寫法
效力跟前面設置相同,`class-validate`的`MaxLength`也會包含在屬性配置當中。
```javascript
export class CreateUserDto {
/**
* The user's name (must not exceed 10 characters).
* @example 'John Doe'
*/
@IsString()
@IsNotEmpty()
@MaxLength(10, { message: 'Name must not exceed 10 characters' })
name: string;
/**
* The user's age in years. Must be a positive number.
* @example 30
*/
@IsPositive()
age: number;
}
```
:::info
如果想要多行的說明,中間必須空白一行(範例如下)
```javascript!
export class CreateUserDto {
/**
* The user's name (must not exceed 10 characters).
*
* 這是第二行的註解說明!!!!
* @example 'John Doe'
*/
@IsString()
@IsNotEmpty()
@MaxLength(10, { message: 'Name must not exceed 10 characters' })
name: string;
}
```
:::
:::danger
如果 @nestjs/swagger 沒有生效,建議砍掉`dist`再嘗試一次。
:::
## 情境 3:GET Method + query params(搭配 Enum 單選用法)
[範例檔案](https://stackblitz.com/edit/nestjs-typescript-starter-gdyxanwd)
app.controller.ts:
```javascript!
import { Controller, Get, Param, Query } from '@nestjs/common';
import { AppService } from './app.service';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { FilterUsersRequestDto } from './dto/filterUserRequest.dto';
import { FilterUsersResponseDto } from './dto/filterUserResponse.dto';
@ApiTags('users')
@Controller('users')
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
@ApiOperation({
summary: '取得全部用戶資訊',
description: '返回系統中所有的用戶資訊,包括用戶名稱、角色和狀態。',
})
@ApiResponse({
status: 200,
description: 'User search successfully.',
type: FilterUsersResponseDto,
})
filterByRole(
@Query() queryParams: FilterUsersRequestDto,
): FilterUsersResponseDto[] {
return this.appService.filterMockData(queryParams);
}
}
```
filterUserRequest.dto.ts:
```javascript!
import { IsEnum, IsOptional, IsString } from 'class-validator';
import { UserRole } from 'src/type/userRole';
import { UserStatus } from 'src/type/userStatus';
export class FilterUsersRequestDto {
/**
* 要篩選的使用者角色
* @example "Admin"
*/
@IsOptional()
@IsEnum(UserRole)
role?: UserRole;
/**
* 使用者狀態(如啟用、停用等)
* @example "active"
*/
@IsOptional()
@IsEnum(UserStatus)
status?: UserStatus;
/**
* 使用者名稱關鍵字(模糊查詢)
* @example "John"
*/
@IsOptional()
@IsString()
name?: string;
}
```
## 情境 4: GET Method path params
[範例檔案](https://stackblitz.com/edit/nestjs-typescript-starter-gdyxanwd)
app.controller.ts:
```javascript!
import { Controller, Get, Param, Query } from '@nestjs/common';
import { AppService } from './app.service';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { FilterUsersRequestDto } from './dto/filterUserRequest.dto';
import { FilterUsersResponseDto } from './dto/filterUserResponse.dto';
@ApiTags('users')
@Controller('users')
export class AppController {
constructor(private readonly appService: AppService) {}
@Get(':userId')
@ApiParam({
name: 'userId',
description: '使用者 ID',
example: 1,
})
@ApiResponse({
status: 200,
description: 'User search successfully.',
type: FilterUsersResponseDto,
})
getUserByid(@Param('userId') userId: number): FilterUsersResponseDto {
return this.appService.filterMockDataById(userId);
}
}
```
:::info
GET Method Param 直接提取參數的做法,swagger 可以自動讀取並生成請求的參數,但並沒有相關描述。
:::
:::danger
- `@Query`或`@Param`,只要是使用物件形式表示,就必須搭配 dto 檔案,讓 @nestjs/swagger 能夠偵測其 JSDoc 語法,生成文件中請求參數的部分。
⬇️ 錯誤示範(讀取不到):
```javascript!
getUserByid(@Param() params: { userId: number}): FilterUsersResponseDto {
return this.appService.filterMockDataById(userId);
}
```
- 如果單一參數,可以考慮直接顯性說明參數範例及描述。例如:@ApiParam 及 @ApiQuery()。
:::
## 情境 5:GET Method query params + 多選 Enum(一般會改使用 POST 設計)
[範例檔案](https://stackblitz.com/edit/nestjs-typescript-starter-dnq6fjrk?file=src%2Fapp.service.ts)
app.controller.ts
```javascript!
import { Controller, Get, Param, Query } from '@nestjs/common';
import { AppService } from './app.service';
import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { FilterUsersRequestDto } from './dto/filterUserRequest.dto';
import { FilterUsersResponseDto } from './dto/filterUserResponse.dto';
@ApiTags('users')
@Controller('users')
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
@ApiOperation({
summary: '取得全部用戶資訊',
description: '返回系統中所有的用戶資訊,包括用戶名稱、角色和狀態。',
})
@ApiResponse({
status: 200,
description: 'User search successfully.',
type: FilterUsersResponseDto,
})
filterByRole(
@Query() queryParams: FilterUsersRequestDto,
): FilterUsersResponseDto[] {
return this.appService.filterMockData(queryParams);
}
}
```
filterUserRequest.dto.ts:
```javascript
export class FilterUsersRequestDto {
/**
* 要篩選的使用者角色(可多選)
* @example ["Admin", "User"]
*/
@IsOptional()
@Transform(({ value }) => (Array.isArray(value) ? value : [value])) // 單一個值會出被當作字串
role?: UserRole[];
// ...略
}
```
:::info
如果多選 Enum 中,僅選擇一個選項,並不會被包裝在 Array 當中。因此透過 Transform 多一層的檢查及轉換。
:::
## 情境 6:formdata 檔案上傳(單個) + file-type 編碼檢查(簡易版不使用自定義 Pipe)
[檔案連結](https://stackblitz.com/edit/nestjs-typescript-starter-fgxkbj4q?file=src%2Fmain.ts)
安裝 Multer:
```npm=
npm install multer @types/multer --save
```
安裝 file-type:
```npm=
npm i file-type
```
:::info
1. multer:Express 檔案上傳中介軟體,可以處理 formdata 資料。
3. file-type:檔案類型檢測工具,透過分析檔案的 magic numbers 編碼格式來判斷實際檔案格式,
:::
:::success
- UseInterceptors:啟用 FileInterceptor 攔截器,處理表單檔案上傳請求。其中 'file' 是表單中的檔案欄位名稱。
- @ApiConsumes('multipart/form-data'):設定請求標題 contetn-type 是 multipart/form-data
- UploadedFile property decorator 取得 request.file 資訊
:::
app.controller.ts
```javascript=
export class AppController {
constructor(private readonly appService: AppService) {}
// 項目一:上傳單個檔案
@Post('file')
@ApiOperation({
summary: '上傳檔案(單個)',
description: '上傳 .png 及 .jpg 的檔案(單個)',
})
@ApiBody({
description: '影像照片檔案',
type: FileUploadDto,
})
@UseInterceptors(FileInterceptor('file'))
@ApiConsumes('multipart/form-data')
async uploadSingleFile(@UploadedFile() file: Express.Multer.File) {
const {
fieldname = '',
originalname = '',
mimetype = '',
buffer = Buffer.alloc(0),
} = file;
const { fileTypeFromBuffer } = await loadEsm<typeof import('file-type')>(
'file-type',
); // 處理 NestJS 中 CommonJS 的匯入方式(編碼檢查套件)
const formatName = Buffer.from(originalname, 'latin1').toString('utf8'); // 避免中文檔名
const { ext = null } = await fileTypeFromBuffer(buffer);
if (!ext || !['png', 'jpg'].includes(ext)) {
throw new BadRequestException('檔案格式錯誤');
}
return { fieldname, formatName, mimetype, ext };
}
}
```
FileUploadDto.dto.ts
```javascript!
import { ApiProperty } from '@nestjs/swagger';
import { Express } from 'express';
export class FileUploadDto {
@ApiProperty({ type: 'string', format: 'binary' })
file: Express.Multer.File;
}
```
## 情境 7:formdata 檔案上傳(多個) + file-type 編碼檢查(簡易版不使用自定義 Pipe)
[檔案連結](https://stackblitz.com/edit/nestjs-typescript-starter-fgxkbj4q?file=src%2Fmain.ts)
:::info
單選及多選的差異比較:
1. UploadedFile => UploadedFiles
2. FileInterceptor => FilesInterceptor,多選情況下可以指定檔案上傳數量
:::
app.controller.ts
```javascript!
export class AppController {
constructor(private readonly appService: AppService) {}
@Post('files')
@UseInterceptors(FilesInterceptor('file', 3)) // 可以指定檔案上傳數量
@ApiOperation({
summary: '上傳檔案(多個)',
description: '上傳 .png 及 .jpg 的檔案(多個)',
})
@ApiBody({
description: '影像照片檔案',
type: FilesUploadDto,
})
@ApiConsumes('multipart/form-data')
async uploadFiles(@UploadedFiles() files: Express.Multer.File[]) {
const { fileTypeFromBuffer } = await loadEsm<typeof import('file-type')>(
'file-type',
); // 處理 NestJS 中 CommonJS 的匯入方式(編碼檢查套件)
const filesInfo = await Promise.all(
files.map(async (file) => {
const {
fieldname = '',
originalname = '',
mimetype = '',
buffer = Buffer.alloc(0),
} = file;
const formatName = Buffer.from(originalname, 'latin1').toString('utf8'); // 避免中文檔名
const { ext = null } = await fileTypeFromBuffer(buffer);
if (!ext || !['png', 'jpg'].includes(ext)) {
throw new BadRequestException('檔案格式錯誤');
}
return { fieldname, formatName, mimetype, ext };
}),
);
return filesInfo;
}
}
```
FilesUploadDto.dto.ts
```javascript!
import { ApiProperty } from '@nestjs/swagger';
export class FilesUploadDto {
@ApiProperty({ type: 'array', items: { type: 'string', format: 'binary' } })
file: Express.Multer.File[];
}
```
## 情境 8:Post Method 雙層陣列(特殊案例)
[檔案連結](https://stackblitz.com/edit/nestjs-typescript-starter-lk5t6edt?file=src%2Fvalidator%2FIsNumberMatrix.ts)
目前無法直接透過裝飾器直接驗證雙層陣列,最多只能驗證到確定這是一個陣列以及確定裡面的項目也是子陣列,但是無法確認子陣列裡面值的型別。
[issue 套件討論](https://github.com/typestack/class-validator/issues/432)
CreateUser.dto
```javascript!
@ApiProperty({
description: '雙層陣列測試',
type: () => [Number],
example: [
[1, 2],
[3, 4],
],
})
@Validate(IsNumberMatrix)
randomList: number[][];
```
:::info
需要搭配手動驗證器 IsNumberMatrix
:::
IsNumberMatrix.ts
```javascript=
import {
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
} from 'class-validator';
/**
* 自定義驗證器:確保 `randomList` 是 `number[][]`
*/
@ValidatorConstraint({ name: 'IsNumberMatrix', async: false })
export class IsNumberMatrix implements ValidatorConstraintInterface {
validate(value: any, args: ValidationArguments) {
console.log('value', value);
if (!Array.isArray(value)) return false; // 確保外層是陣列
return value.every(
(row) =>
Array.isArray(row) && // 確保內部是陣列
row.length > 0 && // 確保子陣列不能為空
row.every((num) => typeof num === 'number'), // 確保內部值是數字
);
}
defaultMessage(args: ValidationArguments) {
return 'randomList must be a two-dimensional array of numbers';
}
}
```