cover

使用 Nest-Swagger 生成 OAS 3.0 接口文档

本文介绍了如何使用 Nest Swagger 插件生成 Open API Specification,以及给予 OAS 3.0 生成 Markdown 格式的接口文档的方法。

2023-12-20

heading

在 Nest 项目中初始化 Swagger

新建一个 Nest 项目:

npm install --global @nestjs/cli nest new use-swagger # 完成项目初始化

添加

依赖:

yarn add @nestjs/swagger

更新

main.ts
文件,添加
swagger
配置:

import { NestFactory } from '@nestjs/core'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { AppModule } from './app.module'; const env = process.env.NODE_ENV | "development"; const port = process.env.PORT | 3000; async function bootstrap() { const app = await NestFactory.create(AppModule); /* ... 其他代码 ... */ if (env == "development) { const config = new DocumentBuilder() .setTitle('Cats example') .setDescription('The cats API description') .setVersion('1.0') .addTag('cats') .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('/api/__doc', app, document); } /* ... 其他代码 ... */ await app.listen(port); } bootstrap();
main.ts

调整后使用

yarn start
启动 Nest 服务,在浏览器中:

heading

常用装饰器

首先创建一个

src/example.dto.ts
文件并添加一下内容:

class SomeParams { @ApiProperty({ name: 'id', description: 'id of Something', example: "1" }) id: string; } class SomeQuery { @ApiProperty({ name: 'page', description: 'page of list', example: 1 }) page: number; @ApiPropertyOptional({ name: 'page_size', description: 'page size of list', default: 10, example: 10, }) page_size?: number; } class SomethingDto { @ApiProperty({ name: 'id', description: 'id of Something', example: 1 }) id: string; @ApiProperty({ name: 'name', description: 'name of Something', example: 'abc' }) name: string; }
src/example.dto.ts

在该文件中定义了接口的参数。

新建

src/example.controller.ts
,并添加以下内容:

import { Body, Controller, Delete, Get, Param, Post, Put, Query, } from '@nestjs/common'; import { ApiBearerAuth, ApiCreatedResponse, ApiExcludeEndpoint, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiProperty, ApiPropertyOptional, ApiTags, } from '@nestjs/swagger'; @ApiTags('Example APIs') @Controller('/api') export class ExampleController { @ApiExcludeEndpoint() @Get('/something/hidden') getSomethingHidden() { return { status: 1 }; } @ApiNotFoundResponse({ description: 'Something Not Found' }) @ApiOkResponse({ description: 'Return Something' }) @ApiOperation({ summary: 'Get Something', description: 'call this API to get Something', }) @Get('/something/:id') getHello(@Param() params: SomeParams, @Query() query: SomeQuery) { // 也可以通过 @ApiParam 和 @ApiQuery 进行定义, // 但是如果有多个 Param 或者 Query 参数很多的话还是使用 class 定义更简洁 return { status: 1, data: { params, query } }; } @ApiCreatedResponse({ description: 'Something Created' }) @ApiBearerAuth() @ApiOperation({ summary: 'Create Something', description: 'call this API to create Something', }) @Post('/something') createSomething(@Body() dto: SomethingDto) { // 也可以通过 @ApiBody 进行定义, // 但是对于结构复杂的 Body 还是使用 class 定义更简洁 return { status: 1, data: dto }; } @ApiOkResponse({ description: 'Return Something' }) @ApiBearerAuth() @ApiOperation({ summary: 'Update Something', description: 'call this API to update Something', }) @Put('/something') updateSomething(@Body() dto: SomethingDto) { return { status: 1, data: dto }; } @ApiOkResponse({ description: 'Return Something' }) @ApiBearerAuth() @ApiOperation({ summary: 'Delete Something', description: 'call this API to delete Something', }) @Delete('/something') deleteSomething() { return { status: 1 }; } }
example.controller.ts

在该文件中定义了五个用于展示装饰器的接口。

ExampleController
添加到
AppModule.controllers
中,并在
main.ts
中的
DocumentBuilder
部分添加
addBearerAuth
以添加
Bearer Token
的输入组件。

async function bootstrap() { /* 其他代码 */ const config = new DocumentBuilder() .setTitle('Cats example') .addBearerAuth() .setDescription('The cats API description') .setVersion('1.0') .addTag('cats') .build(); /* 其他代码 */ }
main.ts

重新启动服务,然后访问文档链接,可以看到如下内容:

ui-1

查看特定路由的详情,可以看到以下内容:

ui-2

常用的

Swagger
装饰器简介如下:

装饰器作用域作用
@ApiTags
Controller定义接口组名称
@ApiOperation
Method定义特定接口信息,包含摘要 summary 和描述 description
@ApiOkResponse
Method定义接口响应状态说明(Ok/NotFound/BadRequest 等)
@ApiBearerAuth
Controller/Method为接口组/接口启用 BearerAuth,使用前需要调用
DocumentBuilder.addBearerAuth
,这样会在 swagger ui 中展示输入 Token 的组件
@ApiExcludedEndpoint
Method忽略该接口,不会再 Swagger 文档中显示
@ApiParam
Method定义接口的路径参数对象,也可以通过
class
并对类中的字段使用
@ApiProperty
进行定义
@ApiQuery
Method定义接口的请求参数对象,也可以通过
class
并对类中的字段使用
@ApiProperty
进行定义
@ApiBody
Method定义接口的 Body 对象,也可以通过
class
并对类中的字段使用
@ApiProperty
进行定义
@ApiProperty
Param/Query/Body定义必填参数信息
@ApiPropertyOptional
Param/Query/Body定义选填参数信息
heading

使用 cli plugins 自动生成 schema

在 Nest.js@10 中支持使用

简化
@ApiProperty
的使用。

heading

配置
nest-swagger
cli-plugin
配置

更新项目的

nest-cli.json
文件,添加
@nestjs/swagger
的插件配置:

{ "collection": "@nestjs/schematics", "sourceRoot": "src", "compilerOptions": { "plugins": [ { "name": "@nestjs/swagger", "options": { "classValidatorShim": true, "introspectComments": true, "controllerFileNameSuffix": ".controller.ts", "dtoFileNameSuffix": ".dto.ts" } } ] } }
nest-cli.json

其中:

  • introspectComments
    指定是否从注释中提取
  • dotFileNameSuffix
    指定 dto 类文件名的后缀
  • controllerFileNameSuffix
    指定 controller 类文件名的后缀
heading

启用
swc
并自动生成
metadata

对于非 monorepo 的 Nest.js 应用,可以通过使用

swc
编译器并开启
--type-check
选项开启自动生成
metadata
。首先安装
swc
编译器:

yarn add --save-dev @swc/cli @swc/core

然后在

nest-cli.json
中添加
builder
配置:

{ "compilerOptions": { /* 其他内容 */ "builder": "swc", "typeCheck": true } }
nest-cli.json

删除

dist
文件夹并重新启动服务:

rm -rf dist yarn start

执行完成后会自动生成

src/metadata.ts
文件,该文件中会包含自动生成的
Swagger Schema
定义。

heading

针对 monorepo 的配置

如果项目使用了 monorepo(lerna)需要添加

src/generate-metadata.ts
文件,并在其中填写如下内容:

import { PluginMetadataGenerator } from '@nestjs/cli/lib/compiler/plugins'; import { ReadonlyVisitor } from '@nestjs/swagger/dist/plugin'; const generator = new PluginMetadataGenerator(); generator.generate({ visitors: [ new ReadonlyVisitor({ introspectComments: true, pathToSource: __dirname }) ], outputDir: __dirname, watch: true, tsconfigPath: 'path/to/api/tsconfig.json', // 后端应用在 monorepo 中的名称 });
src/generate-metadata.ts

然后使用下面的指令生成

metadata.ts
,该文件中会包含自动生成的
Swagger Schema
定义:

npx ts-node path/to/api/src/generate-metadata.ts
heading

使用自动生成的 metadata

首先更新

src/example.dto.ts
,将原先的内容替换为:

export class SomeParams { /** * id of Something * @example 1 */ id: string; } export class SomeQuery { /** * page of the list * @example 1 */ page: number; /** * page size of the list * @example 10 */ page_size?: number = 10; } export class SomethingDto { /** * id of Something * @example 1 */ id: string; /** * name of Something * @example abc */ name: string; }
src/example.dto.ts

在更新后的文件中,移除了

ApiProperty
ApiPropertyOptional
装饰器的使用,在
cli-plugin
生成的
src/metadata.ts
文件中,能够通过
Typescript
的类型定义推断出部分
Swagger Schema
的字段,至于其他内容可以通过注释的方式给出。

通过移除

ApiProperty
ApiPropertyOptional
装饰器的使用,使得 DTO 定义文件的复用性得到了提升。

在完成

src/example.dto.ts
的编辑后,更新
src/main.ts
文件,添加加载自动生成的
src/metadata.ts
的代码:

async function main() // ... const metadata = await import("./metadata").default; await SwaggerModule.loadPluginMetadata(metadata); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup("/api/__doc", app, document); // ... } main();
main.ts

修改完成后使用

yarn start
重启服务,前往浏览器查看最终效果

heading

通过 OAS3.0 数据生成 Markdown 文档

如果需要 Markdown 格式的 API 接口文档,可以使用下面的脚本生成:

# pip install jsonpointer requests pandas from copy import copy import jsonpointer import pandas as pd import requests def convert_schema_shape(input): result_list = [] if 'type' in input and input['type'] == 'object' and 'properties' in input: properties = input['properties'] required = input.get('required', []) for prop_name, prop_data in properties.items(): schema = copy(prop_data) if "description" in schema: schema.pop("description") prop_info = { 'name': prop_name, 'type': prop_data.get('type', 'unknown'), 'description': prop_data.get('description', ''), 'required': prop_name in required, 'schema': schema } result_list.append(prop_info) return result_list def resolve_reference(document, reference): return jsonpointer.resolve_pointer(document, reference) def append_title(method_data, markdown_content: str): if method_data.get('summary', None) is not None: markdown_content += f"## {method_data.get('summary', '')}接口\n\n" else: markdown_content += f"## 接口\n\n" return markdown_content def append_description(method_data, markdown_content: str): markdown_content += f"### 功能说明\n\n" if method_data.get('description', None) is not None: markdown_content += f"{method_data.get('description', '')}\n\n" return markdown_content def append_path(path, markdown_content: str): markdown_content += f"### 调用地址\n\n" markdown_content += f"`{path}`\n\n" return markdown_content def append_method(method, markdown_content: str): markdown_content += f"### 请求方式\n\n" markdown_content += f"{method.upper()}\n\n" return markdown_content def append_content_type(method_data, markdown_content: str): markdown_content += f"### 数据格式\n\n" markdown_content += f"`application/json; encoding=utf-8`\n\n" return markdown_content; def append_parameters(method_data, markdown_content: str): parameters = method_data.get('parameters', []) if parameters: markdown_content += f"### 请求参数\n\n" parameter_table = pd.DataFrame([ ( data.get("name"), data.get("description"), data.get("in"), "是" if data.get("required", "False") else "否", data.get("schema") ) for data in parameters ], columns=["参数名称", "参数说明", "请求类型","是否必须", "schema"]) markdown_content += parameter_table.to_markdown(index=False) + '\n\n' return markdown_content def append_body(openapi_json, method_data, markdown_content: str): request_body = method_data.get('requestBody', {}) if request_body: markdown_content += f"### 请求参数\n\n" content_type = list(request_body.get('content', {}).keys())[0] content_schema = request_body.get('content', {}).get(content_type, {}).get('schema', {}) request_body_ref = content_schema.get('$ref') resolved_schema = resolve_reference(openapi_json, request_body_ref[1:]) body = convert_schema_shape(resolved_schema) body_table = pd.DataFrame([ ( data.get("name"), data.get("description"), "body", "是" if data.get("required", "False") else "否", data.get("schema") ) for data in body ], columns=["参数名称", "参数说明", "请求类型","是否必须", "schema"]) markdown_content += body_table.to_markdown(index=False) + "\n\n" return markdown_content def append_responses(method_data, markdown_content: str): responses = method_data.get('responses', {}) if responses: markdown_content += f"### 响应状态\n\n" response_table = pd.DataFrame([ ( code, data.get('description', ''), "") for code, data in responses.items() ], columns=['状态码', '说明', "备注"]) markdown_content += response_table.to_markdown(index=False) + '\n\n' return markdown_content def append_placeholder(markdown_content: str): markdown_content += f"### 请求样例\n\n" markdown_content += f"### 响应样例\n\n\n\n" return markdown_content def read_and_convert(url, output_file): response = requests.get(url) if response.status_code != 200: print(f"[ERROR] 无法获取 OpenAPI 数据: {response.status_code}") return openapi_json = response.json() if 'paths' not in openapi_json: print("[ERROR] JSON 格式错误:缺少 'paths'") return markdown_content = "# 接口文档\n\n" for path, path_data in openapi_json['paths'].items(): for method, method_data in path_data.items(): markdown_content = append_title(method_data, markdown_content) markdown_content = append_description(method_data, markdown_content) markdown_content = append_path(path, markdown_content) markdown_content = append_method(method, markdown_content) markdown_content = append_content_type(method_data, markdown_content) markdown_content = append_parameters(method_data, markdown_content) markdown_content = append_body(openapi_json, method_data, markdown_content) markdown_content = append_responses(method_data, markdown_content) markdown_content = append_placeholder(markdown_content) # Write to Markdown file with open(output_file, 'w', encoding='utf-8') as file: file.write(markdown_content) print(f"[INFO] 文件 '{output_file}' 生成成功") if __name__ == "__main__": import sys url=sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:3000/api/__doc-json" output=sys.argv[2] if len(sys.argv) > 2 else "doc.md" read_and_convert(url, output) # python to_md.py "http://127.0.0.1:3000/api/__doc-json" "APIs.md"
to_md.py