本系列文已出版成書「NestJS 基礎必學實務指南:使用強大且易擴展的 Node.js 框架打造網頁應用程式」,感謝 iT 邦幫忙與博碩文化的協助。如果對 NestJS 有興趣、覺得這個系列文對你有幫助的話,歡迎前往購書,你的支持是我最大的寫作動力!
如果你是一名前端工程師,那麼你應該會有跟後端要 API 文件的經驗,如果你是一名後端工程師,那你應該會有寫 API 文件的需求,相信很多人都不喜歡花時間在寫文件,甚至要為每個版本做維護,實在是耗時耗力,難道就沒有其他方法來解決這個問題嗎?答案是有的,解決方案就是非常知名的 Swagger。
Swagger 是一套把 API 用視覺化方式呈現的工具,簡單來說,就是會產生一個頁面將各個 API 條列出來,包含了 API 所需的參數以及參數格式等,甚至可以透過這個頁面直接對後端的 API 做操作,達到了 Postman 的效果,大幅降低 API 文件的維護成本,更可以促進前後端的開發效率。
Nest 有把 Swagger 包裝成模組,只需透過 npm
進行安裝,不過這裡需要特別注意除了安裝 Nest 製作的模組外,還需要安裝 Swagger 的套件:
$ npm install @nestjs/swagger swagger-ui-express
注意:如果底層使用 Fastify 的話,就不是安裝
swagger-ui-express
了,而是fastify-swagger
。
接著,我們要在 main.ts
進行初始化,透過 DocumentBuilder
來產生基本的文件格式,可以設置的內容大致上有:標題、描述、版本等,有了格式以後,只需要透過 SwaggerModule
的 createDocument
方法將文件產生出來,並使用 SwaggerModule
的 setup
方法來啟動即可,而 setup
共接受四個參數,分別為:
path
:Swagger UI 的路由。app
:將要綁定的 Nest App 實例帶入。document
:放入初始化文件,即 createDocument
產生的文件。options
:UI 配置選項,為選填項目,接受的參數格式為 SwaggerCustomOptions
。注意:UI 配置選項稍後會再特別說明。
下方為簡單的範例:
import { NestFactory } from '@nestjs/core';
import { INestApplication } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
setupSwagger(app);
await app.listen(3000);
}
function setupSwagger(app: INestApplication) {
const builder = new DocumentBuilder();
const config = builder
.setTitle('TodoList')
.setDescription('This is a basic Swagger document.')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
}
bootstrap();
透過瀏覽器查看 http://localhost:3000/api 會到下方結果:
假如我們要取得該 Swagger 的文件 JSON 檔,可以透過 http://localhost:3000/<PATH>-json
來取得,以上方範例為例,path
為 api
,透過 Postman 存取 http://localhost:3000/api-json 就可以獲得文件 JSON 檔:
可以透過 UI 配置選項來調整 Swagger UI,其較為重要的如下:
explorer
:是否開啟搜尋列,預設為 false
。swaggerOptions
:Swagger 其他配置項目,可以參考官方文件。customCss
:自定義 Swagger UI 的 CSS。customCssUrl
:給予自定義 Swagger UI 的 CSS 資源位址。customJs
:透過自訂 JavaScript 來操作 Swagger UI。customfavIcon
:自訂 Swagger UI icon。swaggerUrl
:給予 Swagger JSON 資源位址。customSiteTitle
:自訂義 Swagger UI 的標題。validatorUrl
:給予 Swagger 的 Validator 資源位址。以下方範例為例,我們將搜尋列開啟:
import { NestFactory } from '@nestjs/core';
import { INestApplication } from '@nestjs/common';
import { DocumentBuilder, SwaggerCustomOptions, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
setupSwagger(app);
await app.listen(3000);
}
function setupSwagger(app: INestApplication) {
const builder = new DocumentBuilder();
const config = builder
.setTitle('TodoList')
.setDescription('This is a basic Swagger document.')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
const options: SwaggerCustomOptions = {
explorer: true, // 開啟搜尋列
};
SwaggerModule.setup('api', app, document, options);
}
bootstrap();
透過瀏覽器查看 http://localhost:3000/api 會發現上方出現了搜尋列:
SwaggerModule
在建置文件的過程中,會去搜尋所有 Controller 底下的路由,並將帶有 @Query
、@Param
以及 @Body
的參數解析出來,進而顯示在 Swagger UI 上,透過這樣的方式不僅能把該 API 所需的參數列出來,還能顯示該參數的型別。
我們先透過 CLI 產生 TodoModule
、TodoController
與 TodoService
:
$ nest generate module features/todo
$ nest generate controller features/todo
$ nest generate service features/todo
接著,我們在 TodoService
設計一個陣列來存放資料,並提供 getTodo
方法來取得指定的內容:
import { Injectable } from '@nestjs/common';
@Injectable()
export class TodoService {
todos = [
{
id: 1,
title: 'Ironman 13th',
description: 'NestJS tutorial.',
completed: false,
},
];
getTodo(id: string) {
return this.todos.find((todo) => todo.id.toString() === id);
}
}
然後調整一下 TodoController
的內容,設計一個透過 id
取得資料的 API:
import { Controller, Get, Param } from '@nestjs/common';
import { TodoService } from './todo.service';
@Controller('todos')
export class TodoController {
constructor(private readonly todoService: TodoService) {}
@Get(':id')
getTodo(@Param('id') id: string) {
return this.todoService.getTodo(id);
}
}
這時候我們可以透過瀏覽器查看 http://localhost:3000/api 會看到 API 的參數與型別都有正確顯示:
雖然說 SwaggerModule
可以自動解析出參數型別,但在面對較為複雜的參數型別就要特別處理才能夠被解析出來,需要特別進行處理的型別有以下幾種:
DTO 是一種物件格式的資料型別,若要讓 SwaggerModule
可以順利解析出該物件中的每個參數的話,需要在每個屬性上使用 @ApiProperty
裝飾器。
我們先在 src/features/todo
資料夾下建立 dto
資料夾並新增 create-todo.dto.ts
,在每個屬相上都添加 @ApiProperty
裝飾器:
import { ApiProperty } from '@nestjs/swagger';
export class CreateTodoDto {
@ApiProperty()
title: string;
@ApiProperty()
description: string;
@ApiProperty()
completed: boolean;
}
接著,我們修改一下 TodoService
的內容,新增一個 createTodo
的方法來添加資料:
import { Injectable } from '@nestjs/common';
import { CreateTodoDto } from './dto/create-todo.dto';
@Injectable()
export class TodoService {
todos = [
{
id: 1,
title: 'Ironman 13th',
description: 'NestJS tutorial.',
completed: false,
},
];
createTodo(data: CreateTodoDto) {
const todo = { id: this.todos.length + 1, ...data };
this.todos.push(todo);
return todo;
}
getTodo(id: string) {
return this.todos.find((todo) => todo.id.toString() === id);
}
}
最後,在 TodoController
添加一個 API 讓我們可以添加資料:
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { TodoService } from './todo.service';
import { CreateTodoDto } from './dto/create-todo.dto';
@Controller('todos')
export class TodoController {
constructor(private readonly todoService: TodoService) {}
@Post()
createTodo(@Body() data: CreateTodoDto) {
return this.todoService.createTodo(data);
}
@Get(':id')
getTodo(@Param('id') id: string) {
return this.todoService.getTodo(id);
}
}
透過瀏覽器查看 http://localhost:3000/api 並點選 [POST] /todos
查看其參數的型別:
如果想要替屬性添加一些選項配置,例如:描述、最大長度等,則可以給參數在 @ApiProperty
裝飾器中,以下方為例,指定 title
最大長度為 20
、description
最大長度為 200
,並描述每個屬性的作用:
import { ApiProperty } from '@nestjs/swagger';
export class CreateTodoDto {
@ApiProperty({
maxLength: 20,
description: 'Todo 的標題',
})
title: string;
@ApiProperty({
maxLength: 200,
description: '描述該 Todo 的細節',
})
description: string;
@ApiProperty({
description: '是否完成該 Todo',
})
completed: boolean;
}
注意:更多選項配置可以參考官方文件。
透過瀏覽器查看 http://localhost:3000/api 並點選 Schemas
裡面的 CreateTodoDto
,會看到下方結果:
陣列也是無法被解析出的型別,這在 DTO 裡面也會遇到含有陣列型別的資料,這時候一樣透過 @ApiProperty
裝飾器即可搞定,給定 type
讓 SwaggerModule
知道這個屬性是陣列型別。
我們調整一下 CreateTodoDto
,添加一個 tags
的屬性:
import { ApiProperty } from '@nestjs/swagger';
export class CreateTodoDto {
@ApiProperty({
maxLength: 20,
description: 'Todo 的標題',
})
title: string;
@ApiProperty({
maxLength: 200,
description: '描述該 Todo 的細節',
})
description: string;
@ApiProperty({
description: '是否完成該 Todo',
})
completed: boolean;
@ApiProperty({
type: [String],
description: '賦予該 Todo 標籤',
})
tags: string[];
}
透過瀏覽器查看 http://localhost:3000/api 並點選 Schemas
裡面的 CreateTodoDto
,會看到下方結果:
還有一種情況比較特殊,如果傳送進來的 主體資料(Body) 是陣列型別的話,就不適合使用 @ApiProperty
來解析,而是要在該方法上套用 @ApiBody
裝飾器,並指定其 type
。
我們修改一下 TodoController
,新增一個批次上傳資料的 API,並添加 @ApiBody
裝飾器以及指定 type
為 [CreateTodoDto]
:
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { ApiBody } from '@nestjs/swagger';
import { TodoService } from './todo.service';
import { CreateTodoDto } from './dto/create-todo.dto';
@Controller('todos')
export class TodoController {
constructor(private readonly todoService: TodoService) {}
@Post()
createTodo(@Body() data: CreateTodoDto) {
return this.todoService.createTodo(data);
}
// 批次上傳
@ApiBody({ type: [CreateTodoDto] })
@Post('bulk')
createTodos(@Body() todos: CreateTodoDto[]) {
return todos.map((todo) => this.todoService.createTodo(todo));
}
@Get(':id')
getTodo(@Param('id') id: string) {
return this.todoService.getTodo(id);
}
}
透過瀏覽器查看 http://localhost:3000/api 並點選 [POST] /todos/bulk
查看其參數的型別:
Enum 也是需要特別做指定的型別,以 DTO 為例,它需要在 @ApiProperty
裝飾器中指定 enum
為特定的 Enum。
我們在 src/features/todo
資料夾下建立 types
資料夾並新增 priority.type.ts
:
export enum TodoPriority {
HIGH = 'high',
MEDIUM = 'medium',
LOW = 'low',
}
接著,我們調整一下 CreateTodoDto
的內容,添加一個 priority
屬性,並帶上 @ApiProperty
裝飾器以及指定 enum
為 TodoPriority
:
import { ApiProperty } from '@nestjs/swagger';
import { TodoPriority } from '../types/priority.type';
export class CreateTodoDto {
@ApiProperty({
maxLength: 20,
description: 'Todo 的標題',
})
title: string;
@ApiProperty({
maxLength: 200,
description: '描述該 Todo 的細節',
})
description: string;
@ApiProperty({
description: '是否完成該 Todo',
})
completed: boolean;
@ApiProperty({
type: [String],
description: '賦予該 Todo 標籤',
})
tags: string[];
// 設置優先權
@ApiProperty({
enum: TodoPriority,
description: '設置該 Todo 的優先權',
})
priority: TodoPriority;
}
透過瀏覽器查看 http://localhost:3000/api 並點選 Schemas
裡面的 CreateTodoDto
,會看到下方結果:
從上面的結果可以看出 Enum 被解析出來了,但如果希望它也能夠成為 Schema 的話,只需要在 @ApiProperty
裝飾器中多添加 enumName
即可,範例如下:
import { ApiProperty } from '@nestjs/swagger';
import { TodoPriority } from '../types/priority.type';
export class CreateTodoDto {
@ApiProperty({
maxLength: 20,
description: 'Todo 的標題',
})
title: string;
@ApiProperty({
maxLength: 200,
description: '描述該 Todo 的細節',
})
description: string;
@ApiProperty({
description: '是否完成該 Todo',
})
completed: boolean;
@ApiProperty({
type: [String],
description: '賦予該 Todo 標籤',
})
tags: string[];
// 設置優先權
@ApiProperty({
enum: TodoPriority,
enumName: 'TodoPriority', // 取名稱讓 Swagger 將其建立成 Schema
description: '設置該 Todo 的優先權',
})
priority: TodoPriority;
}
透過瀏覽器查看 http://localhost:3000/api 並點選 Schemas
,會看到下方結果:
有些結構非常複雜,比如:二維陣列,這種時候該如何配置呢?以 DTO 為例,透過 type
指定為 array
並用型別為物件的 items
來指定該陣列內的型別,因為是二維陣列,故 items
內需要再使用 type
指定為 array
,而這裡的 items
則配置 type
為該二維陣列使用的資料型別。
上面的敘述有點抽象,這裡用實際範例來說明,我們在 CreateTodoDto
內新增一個 something
屬性,它的型別為 string[][]
,並套用 @ApiProperty
裝飾器,接著設置 type
以及 items
來讓 SwaggerModule
可以順利將其型別解析出來:
import { ApiProperty } from '@nestjs/swagger';
import { TodoPriority } from '../types/priority.type';
export class CreateTodoDto {
@ApiProperty({
maxLength: 20,
description: 'Todo 的標題',
})
title: string;
@ApiProperty({
maxLength: 200,
description: '描述該 Todo 的細節',
})
description: string;
@ApiProperty({
description: '是否完成該 Todo',
})
completed: boolean;
@ApiProperty({
type: [String],
description: '賦予該 Todo 標籤',
})
tags: string[];
@ApiProperty({
enum: TodoPriority,
enumName: 'TodoPriority',
description: '設置該 Todo 的優先權',
})
priority: TodoPriority;
// 二維陣列
@ApiProperty({
type: 'array',
items: {
type: 'array',
items: {
type: 'string',
},
},
})
something: string[][];
}
透過瀏覽器查看 http://localhost:3000/api 並點選 Schemas
裡面的 CreateTodoDto
,會看到下方結果:
Swagger 是一個很不錯的工具,套用方法簡單而且也很容易維護,大幅減少了撰寫文件的時間,更可以增進前後端的開發效率,好工具值得一推!下一篇將會繼續介紹 Swagger,把一些我認為比較基礎且常用的功能交代清楚。這裡附上今天的懶人包:
SwaggerModule
。DocumentBuilder
與 createDocument
來產生基本的文件格式。setup
來建置 Swagger UI。SwaggerModule
會去偵測所有 API 帶有 @Body
、@Query
以及 @Param
的參數,進而在 Swagger UI 顯示其參數與型別。@ApiProperty
、@ApiBody
等。