关于 openapi-generator
用于从 API 代码库生成 OpenAPI 3.0 和 3.1 规范的技能,介绍路由扫描、架构提取、框架集成、导出与验证,适合编写 API 文档的开发者。
主要功能
- 01扫描路由并提取架构
- 02生成 OpenAPI 路径与组件
- 03提供 Fastify 和 NestJS 集成示例
- 04导出 YAML 或 JSON 规范
- 05验证生成的规范
适用场景
- 01为代码库中的 API 路由编写文档
- 02创建请求和响应架构
- 03生成供 Swagger UI 或 Redoc 使用的规范
- 04发布前验证 API 规范
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ patricio0312rev/ skills --skill openapi-generator安装后在 Claude Code 中输入/openapi-generator确认已加载。
详细介绍
OpenAPI Generator
从 API 代码库自动生成 OpenAPI 3.0/3.1 规范。
核心工作流
- 扫描路由:查找所有 API 路由定义
- 提取架构:类型、请求/响应正文、参数
- 构建路径:将路由转换为 OpenAPI 路径对象
- 生成架构:根据类型创建组件架构
- 添加文档:描述、示例、标签
- 导出规范:YAML 或 JSON 格式
OpenAPI 3.1 基础模板
openapi: 3.1.0
info:
title: API Title
version: 1.0.0
description: API description
contact:
email: [email protected]
license:
name: MIT
url: https://opensource.org/licenses/MIT
servers:
- url: http://localhost:3000/api
description: Development
- url: https://api.example.com
description: Production
tags:
- name: Users
description: User management endpoints
- name: Products
description: Product catalog endpoints
paths: {}
components:
schemas: {}
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
apiKey:
type: apiKey
in: header
name: X-API-Key
security:
- bearerAuth: []
TypeScript 到 OpenAPI 架构转换器
// scripts/type-to-schema.ts
import * as ts from "typescript";
interface OpenAPISchema {
type?: string;
properties?: Record<string, OpenAPISchema>;
required?: string[];
items?: OpenAPISchema;
$ref?: string;
enum?: string[];
format?: string;
description?: string;
example?: unknown;
}
function typeToOpenAPISchema(
checker: ts.TypeChecker,
type: ts.Type
): OpenAPISchema {
// Handle primitives
if (type.flags & ts.TypeFlags.String) {
return { type: "string" };
}
if (type.flags & ts.TypeFlags.Number) {
return { type: "number" };
}
if (type.flags & ts.TypeFlags.Boolean) {
return { type: "boolean" };
}
// Handle arrays
if (checker.isArrayType(type)) {
const elementType = (type as ts.TypeReference).typeArguments?.[0];
return {
type: "array",
items: elementType ? typeToOpenAPISchema(checker, elementType) : {},
};
}
// Handle object types
if (type.flags & ts.TypeFlags.Object) {
const properties: Record<string, OpenAPISchema> = {};
const required: string[] = [];
type.getProperties().forEach((prop) => {
const propType = checker.getTypeOfSymbolAtLocation(
prop,
prop.valueDeclaration!
);
properties[prop.name] = typeToOpenAPISchema(checker, propType);
// Check if required (no ? modifier)
if (!(prop.flags & ts.SymbolFlags.Optional)) {
required.push(prop.name);
}
});
return {
type: "object",
properties,
required: required.length > 0 ? required : undefined,
};
}
// Handle union types (enums)
if (type.isUnion()) {
const enumValues = type.types
.filter((t) => t.isStringLiteral())
.map((t) => (t as ts.StringLiteralType).value);
if (enumValues.length > 0) {
return { type: "string", enum: enumValues };
}
}
return {};
}
带 JSDoc 的 Express 路由扫描器
// scripts/express-openapi.ts
import * as fs from "fs";
import * as path from "path";
import { parse } from "@babel/parser";
import traverse from "@babel/traverse";
interface RouteMetadata {
method: string;
path: string;
summary?: string;
description?: string;
tags?: string[];
requestBody?: object;
responses?: Record<string, object>;
parameters?: object[];
security?: object[];
}
function extractJSDocMetadata(comments: string): Partial<RouteMetadata> {
const metadata: Partial<RouteMetadata> = {};
// @summary
const summaryMatch = comments.match(/@summary\s+(.+)/);
if (summaryMatch) metadata.summary = summaryMatch[1].trim();
// @description
const descMatch = comments.match(/@description\s+(.+)/);
if (descMatch) metadata.description = descMatch[1].trim();
// @tags
const tagsMatch = comments.match(/@tags\s+(.+)/);
if (tagsMatch) metadata.tags = tagsMatch[1].split(",").map((t) => t.trim());
return metadata;
}
function scanExpressWithOpenAPI(sourceDir: string): RouteMetadata[] {
const routes: RouteMetadata[] = [];
// Implementation: traverse files and extract routes with JSDoc comments
// Similar to postman generator but with OpenAPI-specific metadata
return routes;
}
OpenAPI 路径生成器
// scripts/generate-openapi.ts
import * as yaml from "js-yaml";
interface OpenAPISpec {
openapi: string;
info: object;
servers: object[];
paths: Record<string, object>;
components: {
schemas: Record<string, object>;
securitySchemes?: object;
};
tags?: object[];
security?: object[];
}
function generateOpenAPISpec(
routes: RouteMetadata[],
options: {
title: string;
version: string;
description?: string;
servers: { url: string; description: string }[];
}
): OpenAPISpec {
const spec: OpenAPISpec = {
openapi: "3.1.0",
info: {
title: options.title,
version: options.version,
description: options.description,
},
servers: options.servers,
paths: {},
components: {
schemas: {},
securitySchemes: {
bearerAuth: {
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
},
},
},
tags: [],
};
// Collect unique tags
const tagSet = new Set<string>();
// Generate paths
for (const route of routes) {
const openAPIPath = route.path.replace(/:(\w+)/g, "{$1}");
if (!spec.paths[openAPIPath]) {
spec.paths[openAPIPath] = {};
}
spec.paths[openAPIPath][route.method.toLowerCase()] = {
summary: route.summary || `${route.method} ${route.path}`,
description: route.description,
tags: route.tags || [extractResourceTag(route.path)],
parameters: generateParameters(route),
requestBody: route.requestBody,
responses: route.responses || generateDefaultResponses(route.method),
security: route.security,
};
// Collect tags
(route.tags || [extractResourceTag(route.path)]).forEach((t) =>
tagSet.add(t)
);
}
// Add tags to spec
spec.tags = Array.from(tagSet).map((name) => ({ name }));
return spec;
}
function generateParameters(route: RouteMetadata): object[] {
const params: object[] = [];
// Extract path parameters
const pathParamRegex = /:(\w+)/g;
let match;
while ((match = pathParamRegex.exec(route.path)) !== null) {
params.push({
name: match[1],
in: "path",
required: true,
schema: { type: "string" },
description: `${match[1]} parameter`,
});
}
return params;
}
function generateDefaultResponses(method: string): object {
const responses: Record<string, object> = {
"200": {
description: "Successful response",
content: {
"application/json": {
schema: { type: "object" },
},
},
},
"400": {
description: "Bad request",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
"401": {
description: "Unauthorized",
},
"404": {
description: "Not found",
},
"500": {
description: "Internal server error",
},
};
if (method === "POST") {
responses["201"] = {
description: "Created successfully",
content: {
"application/json": {
schema: { type: "object" },
},
},
};
}
if (method === "DELETE") {
responses["204"] = {
description: "Deleted successfully",
};
}
return responses;
}
function extractResourceTag(path: string): string {
const parts = path.split("/").filter(Boolean);
return parts[0] || "default";
}
常见架构组件
components:
schemas:
Error:
type: object
required:
- code
- message
properties:
code:
type: string
example: "VALIDATION_ERROR"
message:
type: string
example: "Invalid request data"
details:
type: object
additionalProperties:
type: array
items:
type: string
Pagination:
type: object
properties:
page:
type: integer
minimum: 1
example: 1
limit:
type: integer
minimum: 1
maximum: 100
example: 10
total:
type: integer
example: 156
total_pages:
type: integer
example: 16
PaginatedResponse:
type: object
properties:
success:
type: boolean
example: true
data:
type: array
items: {}
meta:
$ref: "#/components/schemas/Pagination"
User:
type: object
required:
- id
- email
- name
properties:
id:
type: string
format: uuid
example: "123e4567-e89b-12d3-a456-426614174000"
email:
type: string
format: email
example: "[email protected]"
name:
type: string
example: "John Doe"
created_at:
type: string
format: date-time
example: "2024-01-15T10:30:00Z"
CreateUserRequest:
type: object
required:
- email
- name
- password
properties:
email:
type: string
format: email
name:
type: string
minLength: 2
maxLength: 100
password:
type: string
format: password
minLength: 8
Fastify 集成
// Fastify with @fastify/swagger
import Fastify from "fastify";
import swagger from "@fastify/swagger";
import swaggerUi from "@fastify/swagger-ui";
const fastify = Fastify({ logger: true });
await fastify.register(swagger, {
openapi: {
info: {
title: "My API",
version: "1.0.0",
},
servers: [{ url: "http://localhost:3000" }],
},
});
await fastify.register(swaggerUi, {
routePrefix: "/docs",
});
// Routes with schema
fastify.get(
"/users/:id",
{
schema: {
params: {
type: "object",
properties: {
id: { type: "string", format: "uuid" },
},
required: ["id"],
},
response: {
200: {
type: "object",
properties: {
id: { type: "string" },
name: { type: "string" },
email: { type: "string" },
},
},
},
},
},
async (request, reply) => {
// Handler
}
);
NestJS 集成
// NestJS with @nestjs/swagger
import { Controller, Get, Post, Body, Param } from "@nestjs/common";
import { ApiTags, ApiOperation, ApiResponse, ApiBody } from "@nestjs/swagger";
@ApiTags("users")
@Controller("users")
export class UsersController {
@Get()
@ApiOperation({ summary: "Get all users" })
@ApiResponse({ status: 200, description: "List of users", type: [UserDto] })
findAll() {
// Implementation
}
@Get(":id")
@ApiOperation({ summary: "Get user by ID" })
@ApiResponse({ status: 200, description: "User found", type: UserDto })
@ApiResponse({ status: 404, description: "User not found" })
findOne(@Param("id") id: string) {
// Implementation
}
@Post()
@ApiOperation({ summary: "Create new user" })
@ApiBody({ type: CreateUserDto })
@ApiResponse({ status: 201, description: "User created", type: UserDto })
create(@Body() createUserDto: CreateUserDto) {
// Implementation
}
}
CLI 脚本
#!/usr/bin/env node
// scripts/openapi-gen.ts
import * as fs from "fs";
import * as yaml from "js-yaml";
import { program } from "commander";
program
.name("openapi-gen")
.description("Generate OpenAPI specification from API routes")
.option("-f, --framework <type>", "Framework (express|nextjs|fastify)", "express")
.option("-s, --source <path>", "Source directory", "./src")
.option("-o, --output <path>", "Output file", "./openapi.yaml")
.option("-t, --title <name>", "API title", "My API")
.option("-v, --version <version>", "API version", "1.0.0")
.option("--json", "Output as JSON instead of YAML")
.parse();
const options = program.opts();
async function main() {
const routes = await scanRoutes(options.framework, options.source);
const spec = generateOpenAPISpec(routes, {
title: options.title,
version: options.version,
servers: [
{ url: "http://localhost:3000/api", description: "Development" },
],
});
const output = options.json
? JSON.stringify(spec, null, 2)
: yaml.dump(spec, { lineWidth: -1 });
fs.writeFileSync(options.output, output);
console.log(`Generated ${options.output} with ${routes.length} endpoints`);
}
main();
验证脚本
// scripts/validate-openapi.ts
import SwaggerParser from "@apidevtools/swagger-parser";
async function validateSpec(specPath: string): Promise<void> {
try {
const api = await SwaggerParser.validate(specPath);
console.log(`API name: ${api.info.title}, Version: ${api.info.version}`);
console.log("OpenAPI specification is valid!");
} catch (err) {
console.error("Validation failed:", err.message);
process.exit(1);
}
}
最佳实践
- 使用 $ref:引用共享架构以避免重复
- 添加示例:为所有架构添加真实示例
- 记录错误:定义所有可能的错误响应
- 使用标签:按资源/功能整理端点
- 版本控制:将规范提交到代码库
- 验证:发布前运行验证
- 生成 SDK:使用 openapi-generator 生成客户端 SDK
- 提供 UI:托管 Swagger UI 或 Redoc 以提供文档
输出检查清单
- 所有路由均已转换为 OpenAPI 路径
- 路径参数使用 {param} 语法
- 请求正文已通过架构定义
- 为所有状态码定义了响应架构
- 常见架构位于 components/schemas 中
- 已配置安全架构
- 所有端点均已应用标签
- 架构中包含示例
- 规范验证无错误
- YAML/JSON 已成功导出
评论
支持 Markdown