一个优雅的从 OpenAPI/Swagger 规范生成 TypeScript API 请求层代码的工具
# 1. 安装工具
npm install -g openapi-typescript-cli
# 2. 生成代码(从本地文件)
openapi-typescript-cli -f ./openapi.json -n api
# 3. 生成代码(从远程 URL)
openapi-typescript-cli -u http://localhost:8080/v3/api-docs -n api
# 4. 使用中间件自定义命名
openapi-typescript-cli -f ./openapi.json -n api -m ./middleware.jsnpm install -g openapi-typescript-clinpm install --save-dev openapi-typescript-cli然后在 package.json 中添加脚本:
{
"scripts": {
"generate:api": "openapi-typescript-cli -f ./openapi.json -n api"
}
}openapi-typescript-cli -f ./openapi.json -n api生成的文件:
api.ts- API 请求函数api.d.ts- TypeScript 类型定义request.js- Axios 封装(首次生成)middleware.example.js- 中间件示例(首次生成)
openapi-typescript-cli -u http://localhost:8080/v3/api-docs -n api支持的 URL 格式:
- Spring Boot:
http://localhost:8080/v3/api-docs - Swagger UI:
http://localhost:8080/swagger.json - 其他符合 OpenAPI 3.0 规范的 JSON 端点
| 参数 | 简写 | 说明 | 必填 | 默认值 |
|---|---|---|---|---|
--apifile |
-f |
OpenAPI JSON 文件路径 | 否* | - |
--url |
-u |
OpenAPI JSON 文件 URL 地址 | 否* | - |
--name |
-n |
输出文件名称 | 否 | index |
--middleware |
-m |
中间件文件路径 | 否 | - |
注意: --apifile 或 --url 至少需要提供一个。
# 使用本地文件
openapi-typescript-cli -f ./openapi.json -n api
# 使用远程 URL
openapi-typescript-cli -u http://localhost:8080/v3/api-docs -n api
# 指定输出文件名
openapi-typescript-cli -f ./openapi.json -n petstore
# 使用中间件
openapi-typescript-cli -f ./openapi.json -n api -m ./middleware.js中间件用于自定义生成的模块名和函数名。
创建 middleware.js:
module.exports = function ({operationId, description, path, method, tag, operation}) {
return {
moduleName: tag, // 模块名,对应 export let xxx
functionName: operationId || '', // 函数名
}
}module.exports = function ({operationId, description, path, method, tag, operation}) {
// 示例:从路径提取模块名
// 路径:/api/v1/user/profile
// 提取:user
const pathParts = path.split('/').filter(p => p);
const moduleName = pathParts[1] || tag; // 使用路径的第二段作为模块名
// 示例:重命名函数
let functionName = operationId;
if (operationId === 'getUserById') {
functionName = 'fetchUser';
}
return {
moduleName: moduleName,
functionName: functionName,
}
}openapi-typescript-cli -f ./openapi.json -n api -m ./middleware.js// 从 components.schemas 生成的接口
export interface Pet {
id?: number;
name: string; // 必需字段
photoUrls: string[]; // 必需字段
status?: 'available' | 'pending' | 'sold'; // Enum → Union Type
}
export interface Order {
shipDate?: Date; // date-time → Date
status?: 'placed' | 'approved' | 'delivered';
}
// 查询参数类型
export interface QueryTypefindPetsByStatus {
status?: 'available' | 'pending' | 'sold';
}
// 路径参数类型
export interface PathTypegetPetById {
petId: number; // 路径参数必需
}import request from "./request"
import { AxiosRequestConfig } from 'axios'
import * as Type from './api.d'
export let pet = {
/**
* Find pet by ID.
* Returns a single pet.
* @tags pet
*/
getPetById: async (
param: Type.PathTypegetPetById,
opt: AxiosRequestConfig = {}
): Promise<Type.Pet> => await request({
url: `/pet/${param.petId}`, // 路径参数使用模板字符串
method: 'get',
...opt,
}),
findPetsByStatus: async (
param: Type.QueryTypefindPetsByStatus,
opt: AxiosRequestConfig = {}
): Promise<Type.Pet[]> => await request({
url: '/pet/findByStatus',
method: 'get',
params: {status: param?.status,},
...opt,
}),
}// 这个文件在首次生成后不会被覆盖
// 你可以安全地自定义拦截器、错误处理等
import axios from 'axios'
const instance = axios.create({
baseURL: '/',
timeout: 10000,
});
// 请求拦截器
instance.interceptors.request.use((config) => {
// 添加 token、loading 等
return config;
});
// 响应拦截器
instance.interceptors.response.use(
(res) => {
// 处理响应数据
return res.data;
},
(error) => {
// 处理错误
return Promise.reject(error);
}
);
export default instance;工具会根据 OpenAPI 规范自动优化生成的类型:
// OpenAPI
{
"status": {
"type": "string",
"enum": ["available", "pending", "sold"]
}
}
// 生成的 TypeScript
status?: 'available' | 'pending' | 'sold';// OpenAPI
{
"required": ["name", "photoUrls"],
"properties": {
"name": {"type": "string"},
"photoUrls": {"type": "array"}
}
}
// 生成的 TypeScript
export interface Pet {
name: string; // 必需(无 ?)
photoUrls: string[]; // 必需(无 ?)
id?: number; // 可选(有 ?)
}// OpenAPI
{
"shipDate": {
"type": "string",
"format": "date-time"
}
}
// 生成的 TypeScript
shipDate?: Date; // date-time → Date// 路径参数类型(必需)
export interface PathTypegetPetById {
petId: number;
}
// 查询参数类型(可选)
export interface QueryTypefindPetsByStatus {
status?: 'available' | 'pending' | 'sold';
}
// 组合使用
updatePetWithForm: async (
param: Type.PathTypeupdatePetWithForm & Type.QueryTypeupdatePetWithForm,
...
) => ...// 生成正确的数组类型
Promise<Type.Pet[]> // ✅ 正确
param: Type.User[] // ✅ 正确// OpenAPI additionalProperties
{
"type": "object",
"additionalProperties": {
"type": "integer"
}
}
// 生成的 TypeScript
Promise<{ [key: string]: number }> // ✅ 正确,无 Type. 前缀A: 如果生成的接口代码有语法错误,大概率是后端 OpenAPI 文档有语法错误。
常见问题:
- Schema 定义不完整 - 缺少
type字段 - 参数定义错误 -
in字段缺失或错误 - 响应定义缺失 - 缺少
responses定义 - 类型不匹配 -
type与format不匹配
解决方法:
- 使用 Swagger Editor 验证 OpenAPI 文档
- 检查后端代码中的注解是否正确
- 参考 OpenAPI 规范文档
A: 检查后端 OpenAPI 文档中的类型定义。
确保后端正确使用了:
@Schema注解(Java Spring Boot)required字段enum定义format定义(date-time, int64 等)
A: 检查后端路径参数定义。
确保 OpenAPI 文档中:
{
"parameters": [
{
"name": "petId",
"in": "path", // ✅ 必须是 "path"
"required": true, // ✅ 路径参数必须 required
"schema": {
"type": "integer"
}
}
]
}A: 使用中间件自定义。
创建 middleware.js 文件自定义模块名和函数名。
A: 直接重新运行生成命令。
生成的 request.js 不会被覆盖,其他文件会被覆盖。
如果生成的接口代码有错误,99% 的情况是后端 OpenAPI 文档的语法错误或定义不完整。
工具本身只是忠实地将 OpenAPI 规范转换为 TypeScript 代码。如果后端文档有问题,生成的结果也会有问题。
// ❌ 错误示例
{
"properties": {
"name": {} // 缺少 type 字段
}
}
// ✅ 正确示例
{
"properties": {
"name": {
"type": "string"
}
}
}// ❌ 错误示例
{
"parameters": [
{
"name": "petId"
// 缺少 in、schema 字段
}
]
}
// ✅ 正确示例
{
"parameters": [
{
"name": "petId",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
}
]
}// ❌ 错误示例
{
"get": {
"operationId": "getPet"
// 缺少 responses 字段
}
}
// ✅ 正确示例
{
"get": {
"operationId": "getPet",
"responses": {
"200": {
"description": "successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
}
}
}
}
}
}// ❌ 错误示例
{
"type": "string",
"format": "int64" // format 应该是 integer 的
}
// ✅ 正确示例
{
"type": "integer",
"format": "int64"
}- 访问 https://editor.swagger.io/
- 将后端生成的 OpenAPI JSON 粘贴进去
- 查看是否有错误提示(红色标记)
# 安装 swagger-codegen-cli
npm install -g @openapitools/openapi-generator-cli
# 验证文档
openapi-generator-cli validate -i ./openapi.jsonSpring Boot 示例:
// ✅ 正确示例
@RestController
@RequestMapping("/api/pet")
public class PetController {
@GetMapping("/{petId}")
@Operation(summary = "Find pet by ID")
@ApiResponse(
responseCode = "200",
description = "successful operation",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = Pet.class)
)
)
public ResponseEntity<Pet> getPetById(
@Parameter(description = "ID of pet to return", required = true)
@PathVariable Integer petId
) {
// ...
}
}常见注解错误:
// ❌ 错误:缺少 @Parameter 注解
@GetMapping("/{petId}")
public ResponseEntity<Pet> getPetById(@PathVariable Integer petId) {
// ...
}
// ✅ 正确:完整的注解
@GetMapping("/{petId}")
@Operation(summary = "Find pet by ID")
@ApiResponse(responseCode = "200", ...)
public ResponseEntity<Pet> getPetById(
@Parameter(description = "ID of pet", required = true)
@PathVariable Integer petId
) {
// ...
}在生成代码前,确保后端 OpenAPI 文档:
- 所有 Schema 都有
type字段 - 所有参数都有
in字段(path/query/header) - 所有操作都有
responses定义 - 路径参数
required: true - Enum 使用
enum字段定义 - 必需字段使用
required数组 - Date 类型使用
format: "date-time" - 使用 Swagger Editor 验证无错误
- 使用完整的注解:确保所有接口都有完整的 Swagger/OpenAPI 注解
- 定义 Schema:将所有实体类定义为
@Schema,并在components.schemas中引用 - 明确类型:使用正确的
type和format - 必需字段:正确使用
required数组
- 版本控制:将生成的代码纳入版本控制,但不要手动修改
- 定期更新:后端接口变更后,及时重新生成代码
- 自定义 Request:在
request.js中添加统一的拦截器、错误处理 - 使用中间件:通过中间件统一命名规范
- 文档优先:后端先完善 OpenAPI 文档,再让前端生成代码
- 验证流程:在生成代码前,使用 Swagger Editor 验证文档
- 错误反馈:如果生成错误,先检查后端文档,再反馈给后端团队
# 1. 先验证 OpenAPI 文档
# 在 Swagger Editor 中验证
# 2. 使用中间件调试
# 在 middleware.js 中添加 console.log 查看原始数据
# 3. 检查生成的代码
# 查看生成的文件,定位问题所在
# 4. 对比后端文档
# 检查对应接口的 OpenAPI 定义是否正确- GitHub: https://github.com/cnvoid/openapi-typescript-cli
- 问题反馈: 请在 GitHub Issues 中提交
- 文档: 查看
docs/目录下的更多文档
-
工具是代码生成器,不是文档校验器
- 如果生成的代码有错误,先检查后端 OpenAPI 文档
-
使用 Swagger Editor 验证文档
- 这是验证 OpenAPI 文档最可靠的方法
-
与后端团队协作
- 确保后端提供完整、正确的 OpenAPI 文档
-
定期更新生成
- 后端接口变更后及时重新生成代码
-
善用中间件
- 通过中间件统一命名规范和自定义逻辑
记住:生成工具的质量取决于输入文档的质量! 📝✅