iT邦幫忙

2021 iThome 鐵人賽

DAY 26
0
Modern Web

NestJS 帶你飛!系列 第 26

[NestJS 帶你飛!] DAY26 - Swagger (上)

如果你是一名前端工程師,那麼你應該會有跟後端要 API 文件的經驗,如果你是一名後端工程師,那你應該會有寫 API 文件的需求,相信很多人都不喜歡花時間在寫文件,甚至要為每個版本做維護,實在是耗時耗力,難道就沒有其他方法來解決這個問題嗎?答案是有的,解決方案就是非常知名的 Swagger

什麼是 Swagger?

https://ithelp.ithome.com.tw/upload/images/20210722/20119338RloglKEMqo.png

圖片來源

Swagger 是一套把 API 用視覺化方式呈現的工具,簡單來說,就是會產生一個頁面將各個 API 條列出來,包含了 API 所需的參數以及參數格式等,甚至可以透過這個頁面直接對後端的 API 做操作,達到了 Postman 的效果,大幅降低 API 文件的維護成本,更可以促進前後端的開發效率。

初探 Swagger

Nest 有把 Swagger 包裝成模組,只需透過 npm 進行安裝,不過這裡需要特別注意除了安裝 Nest 製作的模組外,還需要安裝 Swagger 的套件:

$ npm install @nestjs/swagger swagger-ui-express

注意:如果底層使用 Fastify 的話,就不是安裝 swagger-ui-express 了,而是 fastify-swagger

接著,我們要在 main.ts 進行初始化,透過 DocumentBuilder 來產生基本的文件格式,可以設置的內容大致上有:標題、描述、版本等,有了格式以後,只需要透過 SwaggerModulecreateDocument 方法將文件產生出來,並使用 SwaggerModulesetup 方法來啟動即可,而 setup 共接受四個參數,分別為:

  1. path:Swagger UI 的路由。
  2. app:將要綁定的 Nest App 實例帶入。
  3. document:放入初始化文件,即 createDocument 產生的文件。
  4. 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 會到下方結果:
https://ithelp.ithome.com.tw/upload/images/20210726/201193387ZoxohnKeV.png

假如我們要取得該 Swagger 的文件 JSON 檔,可以透過 http://localhost:3000/<PATH>-json 來取得,以上方範例為例,pathapi,透過 Postman 存取 http://localhost:3000/api-json 就可以獲得文件 JSON 檔:
https://ithelp.ithome.com.tw/upload/images/20210727/20119338P4TkegX7g9.png

UI 配置選項

可以透過 UI 配置選項來調整 Swagger UI,其較為重要的如下:

  1. explorer:是否開啟搜尋列,預設為 false
  2. swaggerOptions:Swagger 其他配置項目,可以參考官方文件
  3. customCss:自定義 Swagger UI 的 CSS。
  4. customCssUrl:給予自定義 Swagger UI 的 CSS 資源位址。
  5. customJs:透過自訂 JavaScript 來操作 Swagger UI。
  6. customfavIcon:自訂 Swagger UI icon。
  7. swaggerUrl:給予 Swagger JSON 資源位址。
  8. customSiteTitle:自訂義 Swagger UI 的標題。
  9. 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 會發現上方出現了搜尋列:
https://ithelp.ithome.com.tw/upload/images/20210728/20119338QA13j87Odq.png

API 參數設計

SwaggerModule 在建置文件的過程中,會去搜尋所有 Controller 底下的路由,並將帶有 @Query@Param 以及 @Body 的參數解析出來,進而顯示在 Swagger UI 上,透過這樣的方式不僅能把該 API 所需的參數列出來,還能顯示該參數的型別。

我們先透過 CLI 產生 TodoModuleTodoControllerTodoService

$ 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 的參數與型別都有正確顯示:
https://ithelp.ithome.com.tw/upload/images/20210802/20119338KCJCeqY48I.png

解析複雜型別

雖然說 SwaggerModule 可以自動解析出參數型別,但在面對較為複雜的參數型別就要特別處理才能夠被解析出來,需要特別進行處理的型別有以下幾種:

DTO

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 查看其參數的型別:

https://ithelp.ithome.com.tw/upload/images/20210802/20119338sVbdJCW4XK.png

如果想要替屬性添加一些選項配置,例如:描述、最大長度等,則可以給參數在 @ApiProperty 裝飾器中,以下方為例,指定 title 最大長度為 20description 最大長度為 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,會看到下方結果:

https://ithelp.ithome.com.tw/upload/images/20210802/2011933881cljtD56g.png

陣列

陣列也是無法被解析出的型別,這在 DTO 裡面也會遇到含有陣列型別的資料,這時候一樣透過 @ApiProperty 裝飾器即可搞定,給定 typeSwaggerModule 知道這個屬性是陣列型別。

我們調整一下 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,會看到下方結果:

https://ithelp.ithome.com.tw/upload/images/20210802/201193389zqrDcsjLZ.png

還有一種情況比較特殊,如果傳送進來的 主體資料(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 查看其參數的型別:

https://ithelp.ithome.com.tw/upload/images/20210802/20119338fZANS3Xu1L.png

Enum

Enum 也是需要特別做指定的型別,以 DTO 為例,它需要在 @ApiProperty 裝飾器中指定 enum 為特定的 Enum。

我們在 src/features/todo 資料夾下建立 types 資料夾並新增 priority.type.ts

export enum TodoPriority {
  HIGH = 'high',
  MEDIUM = 'medium',
  LOW = 'low',
}

接著,我們調整一下 CreateTodoDto 的內容,添加一個 priority 屬性,並帶上 @ApiProperty 裝飾器以及指定 enumTodoPriority

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,會看到下方結果:

https://ithelp.ithome.com.tw/upload/images/20210802/20119338n2H6VPXCnS.png

從上面的結果可以看出 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,會看到下方結果:

https://ithelp.ithome.com.tw/upload/images/20210802/20119338moDWkR9Ta6.png

巢狀複雜結構

有些結構非常複雜,比如:二維陣列,這種時候該如何配置呢?以 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,會看到下方結果:

https://ithelp.ithome.com.tw/upload/images/20210802/20119338ljiX0NBg0O.png

小結

Swagger 是一個很不錯的工具,套用方法簡單而且也很容易維護,大幅減少了撰寫文件的時間,更可以增進前後端的開發效率,好工具值得一推!下一篇將會繼續介紹 Swagger,把一些我認為比較基礎且常用的功能交代清楚。這裡附上今天的懶人包:

  1. Swagger 是一套把 API 用視覺化方式呈現的工具。
  2. Nest 有將 Swagger 包裝成模組,其名為 SwaggerModule
  3. 透過 DocumentBuildercreateDocument 來產生基本的文件格式。
  4. 透過 setup 來建置 Swagger UI。
  5. SwaggerModule 會去偵測所有 API 帶有 @Body@Query 以及 @Param 的參數,進而在 Swagger UI 顯示其參數與型別。
  6. DTO 等複雜型別需要進行特殊處理,例如:@ApiProperty@ApiBody 等。

上一篇
[NestJS 帶你飛!] DAY25 - Authorization & RBAC
下一篇
[NestJS 帶你飛!] DAY27 - Swagger (下)
系列文
NestJS 帶你飛!32

尚未有邦友留言

立即登入留言