使用 Nest-Swagger 生成 OAS 3.0 接口文档
本文介绍了如何使用 Nest Swagger 插件生成 Open API Specification,以及给予 OAS 3.0 生成 Markdown 格式的接口文档的方法。
2023-12-20
在 Nest 项目中初始化 Swagger
新建一个 Nest 项目:
npm install --global @nestjs/cli
nest new use-swagger
# 完成项目初始化
添加 依赖:nest-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
- 访问 http://localhost:3000/api/__doc 查看在线文档;
- 访问 http://localhost:3000/api/__doc-json 查看 JSON 格式的文档数据。
常用装饰器
首先创建一个
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
重新启动服务,然后访问文档链接,可以看到如下内容:
查看特定路由的详情,可以看到以下内容:
常用的
Swagger
装饰器 | 作用域 | 作用 |
---|---|---|
@ApiTags | Controller | 定义接口组名称 |
@ApiOperation | Method | 定义特定接口信息,包含摘要 summary 和描述 description |
@ApiOkResponse | Method | 定义接口响应状态说明(Ok/NotFound/BadRequest 等) |
@ApiBearerAuth | Controller/Method | 为接口组/接口启用 BearerAuth,使用前需要调用 DocumentBuilder.addBearerAuth |
@ApiExcludedEndpoint | Method | 忽略该接口,不会再 Swagger 文档中显示 |
@ApiParam | Method | 定义接口的路径参数对象,也可以通过 class @ApiProperty |
@ApiQuery | Method | 定义接口的请求参数对象,也可以通过 class @ApiProperty |
@ApiBody | Method | 定义接口的 Body 对象,也可以通过 class @ApiProperty |
@ApiProperty | Param/Query/Body | 定义必填参数信息 |
@ApiPropertyOptional | Param/Query/Body | 定义选填参数信息 |
使用 cli plugins 自动生成 schema
在 Nest.js@10 中支持使用 简化 cli-plugin
@ApiProperty
配置 nest-swagger
的 cli-plugin
配置
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
- 指定 dto 类文件名的后缀
dotFileNameSuffix
- 指定 controller 类文件名的后缀
controllerFileNameSuffix
启用 swc
并自动生成 metadata
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
针对 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
使用自动生成的 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
在完成
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
通过 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