告别手写 API 类型:用 openapi-fetch 打造类型安全的前端接口层

你有没有遇到过这样的场景:后端改了一个接口字段,前端完全不知道,直到上线后用户反馈才发现数据异常?或者你在写 axios.post('/user/list', params) 时,根本不知道 params 该传什么字段,只能去翻 Swagger 文档对照着写?

这篇文章介绍一套工具链,让这两个问题彻底消失。


问题的根源

传统前端 API 调用长这样:

ts 复制代码
// 你不知道 body 该是什么结构
const res = await axios.post('/persona/config/save', body)
// 你不知道 res.data 里有什么字段
const data = res.data.data

类型是 any,IDE 没有提示,出错只能靠运行时发现。

根源在于:后端有完整的 API 描述(Swagger/OpenAPI 文档),但前端没有消费这份描述

解决方案就是把 OpenAPI 文档转成 TypeScript 类型,再用类型安全的 HTTP 客户端调用它。整个工具链涉及四个工具:

复制代码
zx  →  swagger2openapi  →  openapi-typescript  →  openapi-fetch

工具一:zx ------ 让 Node.js 脚本告别 child_process

它是什么

zx 是 Google 出品的 Node.js 脚本工具。核心思想是:写脚本的人更熟悉 JavaScript,但 shell 命令执行又很麻烦。zx 让你在 .js 文件里直接执行 shell 命令。

对比传统写法

js 复制代码
// 传统 child_process,冗长且难处理异步
const { exec } = require('child_process')
exec('ls -la', (err, stdout) => {
  if (err) throw err
  console.log(stdout)
})

// zx,简洁直观
import { $ } from 'zx'
const result = await $`ls -la`
console.log(result.stdout)

核心特性

模板字符串执行命令

js 复制代码
import { $ } from 'zx'

// 执行命令,自动处理转义
await $`echo "hello world"`

// 变量插值(自动安全转义,防注入)
const filename = 'my file.txt'
await $`cat ${filename}`  // 不会因为空格出问题

管道支持

js 复制代码
// shell 管道
await $`swagger2openapi http://api.example.com/v2/api-docs | openapi-typescript -o schema.ts`

错误处理

js 复制代码
try {
  await $`cat non-existent-file`
} catch (err) {
  console.error(`Exit code: ${err.exitCode}`)
  console.error(`Stderr: ${err.stderr}`)
}

安装

bash 复制代码
pnpm add -D zx

zx 使用 ES Module 语法(import / await),运行前需要确保 JS 文件被识别为 ESM。有三种方式:

方式一(推荐):package.json"type": "module"

json 复制代码
// package.json
{ "type": "module" }

加了之后,项目内所有 .js 文件默认按 ESM 处理,直接用 node 运行:

bash 复制代码
node generate_apis.js

方式二:文件后缀改为 .mjs

不改 package.json,把脚本文件命名为 generate_apis.mjsnode 会自动识别为 ESM:

bash 复制代码
node generate_apis.mjs

方式三:用 zx CLI 直接运行

zx 自带命令行工具,无需关心 ESM 配置,直接运行:

bash 复制代码
npx zx generate_apis.js

实际项目中通常把命令写进 package.jsonscripts,用包管理器调用:

bash 复制代码
pnpm gen:api   # 等价于 node generate_apis.js 或 zx generate_apis.js

工具二:swagger2openapi ------ 格式转换桥梁

为什么需要它

OpenAPI 规范有两个主要版本:

  • Swagger 2.0 :老版本,/v2/api-docs 端点,大量 Spring Boot 项目在用
  • OpenAPI 3.0 :新版本,/v3/api-docs 端点,功能更强

openapi-typescript 只支持 OpenAPI 3.0。如果你的后端接口文档是 Swagger 2.0,就需要 swagger2openapi 先做一次转换。

用法

bash 复制代码
# 直接转换并输出
swagger2openapi http://your-api.com/v2/api-docs

# 配合管道,转换后直接输入给 openapi-typescript
swagger2openapi http://your-api.com/v2/api-docs | openapi-typescript -o schema.ts

如何判断用哪个

后端文档地址 版本 是否需要 swagger2openapi
/v2/api-docs Swagger 2.0 需要
/v3/api-docs OpenAPI 3.0 不需要

安装

bash 复制代码
pnpm add -D swagger2openapi

工具三:openapi-typescript ------ 把 API 文档变成 TypeScript 类型

它做了什么

openapi-typescript 读取 OpenAPI 规范文档,生成对应的 TypeScript 类型定义文件。这个文件完整描述了每一个接口的路径、请求参数、请求体、响应结构。

输出示例

假设后端有一个接口:

yaml 复制代码
POST /persona/config/save
requestBody:
  application/json:
    personaName: string (required)
    personaAge: number
responses:
  200:
    application/json:
      code: number
      data: { id: number }

openapi-typescript 会生成:

ts 复制代码
// server-schema.ts(自动生成,不要手动修改)
export interface paths {
  "/persona/config/save": {
    post: {
      requestBody: {
        content: {
          "application/json": {
            personaName: string
            personaAge?: number
          }
        }
      }
      responses: {
        200: {
          content: {
            "application/json": {
              code: number
              data: { id: number }
            }
          }
        }
      }
    }
  }
}

用法

bash 复制代码
# OpenAPI 3.0 接口,直接生成
openapi-typescript http://your-api.com/v3/api-docs -o src/api/server-schema.ts

# Swagger 2.0 接口,先转换再生成
swagger2openapi http://your-api.com/v2/api-docs | openapi-typescript -o src/api/server-schema.ts

# 本地文件
openapi-typescript ./openapi.json -o src/api/server-schema.ts

安装

bash 复制代码
pnpm add -D openapi-typescript

最佳实践

把命令写进 package.json,与团队共享:

json 复制代码
{
  "scripts": {
    "gen:api": "node generate_apis.js"
  }
}

generate_apis.js(用 zx 编写,支持多服务):

js 复制代码
import { $ } from 'zx'

// Swagger 2.0 服务
await $`swagger2openapi http://service-a.com/v2/api-docs | openapi-typescript -o src/api/serviceA/server-schema.ts`

// OpenAPI 3.0 服务
await $`openapi-typescript http://service-b.com/v3/api-docs -o src/api/serviceB/server-schema.ts`

后端接口有变更时,运行一条命令即可同步:

bash 复制代码
pnpm gen:api

工具四:openapi-fetch ------ 类型安全的 HTTP 客户端

它解决了什么

有了 server-schema.ts,还需要一个能"消费"这份类型的 HTTP 客户端。axios 做不到,因为它不知道每个 URL 对应什么类型。

openapi-fetch 就是为此而生的。它接受 paths 类型作为泛型参数,使得:

  • 输入 URL 时有自动补全,路径写错会报错
  • body / params 有完整类型约束,字段写错编译失败
  • 响应的 data 有精确类型推断,不再是 any

核心概念

1. 创建 client
ts 复制代码
// src/api/client.ts
import createClient from 'openapi-fetch'
import type { paths } from './server-schema.ts'

export const apiClient = createClient<paths>({
  baseUrl: 'https://your-api.com'
})

apiClient.use(middleware);

createClient<paths> 这一步是关键,它把所有接口类型绑定到这个 client 实例上。

2. Middleware(中间件)

等价于 axios 的拦截器,处理鉴权和统一错误处理:

ts 复制代码
// src/api/middleware.ts
import type { Middleware } from 'openapi-fetch'

export const authMiddleware: Middleware = {
  // 请求前:注入 token
  async onRequest({ request }) {
    const token = await getToken()
    request.headers.set('Authorization', `Bearer ${token}`)
    return request
  },
  // 响应后:统一处理错误
  async onResponse({ response }) {
    if (response.status === 401) {
      // 跳转登录
      return
    }
    if (response.status >= 400) {
      const body = await response.clone().json()
      throw new Error(body?.message || '请求失败')
    }
    return response
  }
}

// 挂载中间件
apiClient.use(authMiddleware)
3. 发起请求
ts 复制代码
import { apiClient } from '@/api/client'

// GET 请求(query 参数)
const { data, error } = await apiClient.GET('/persona/enum/list', {
  params: {
    query: { type: 'voice' }  // 类型安全,写错字段名会报错
  }
})

// POST 请求(body)
const { data, error } = await apiClient.POST('/persona/config/save', {
  body: {
    personaName: '助手A',  // 少传 required 字段会报错
    personaAge: 25
  }
})

// 处理响应
if (error) {
  console.error('请求失败', error)
} else {
  console.log(data.id)  // data 有精确类型,IDE 有提示
}

与 axios 的核心差异

对比项 axios openapi-fetch
URL 类型检查 无,字符串 有,仅允许 schema 中存在的路径
请求参数类型 any 与 OpenAPI schema 完全对应
响应类型 手动断言 as Xxx 自动推断
鉴权/错误处理 拦截器 Middleware
底层实现 XMLHttpRequest Fetch API

完整接入流程

第一步:安装依赖

bash 复制代码
# 运行时依赖
pnpm add openapi-fetch

# 开发依赖(代码生成工具)
pnpm add -D openapi-typescript swagger2openapi zx

第二步:编写生成脚本

js 复制代码
// generate_apis.js
import { $ } from 'zx'

console.log('Generating API schema...')
await $`openapi-typescript http://your-api.com/v3/api-docs -o src/api/server-schema.ts`
console.log('Done.')
json 复制代码
// package.json
{
  "scripts": {
    "gen:api": "node generate_apis.js"
  }
}

第三步:首次生成类型文件

bash 复制代码
pnpm gen:api
# 生成 src/api/server-schema.ts

第四步:创建 middleware

ts 复制代码
// src/api/middleware.ts
import type { Middleware } from 'openapi-fetch'
import { getToken, logout } from '@/auth'
import { showError } from '@/utils'

export const middleware: Middleware = {
  async onRequest({ request }) {
    const token = await getToken()
    if (token) request.headers.set('Authorization', `Bearer ${token}`)
    return request
  },
  async onResponse({ response }) {
    if (response.status === 401) { await logout(); return }
    if (response.status > 401 && response.status <= 500) {
      const body = await response.clone().json().catch(() => ({}))
      showError(body?.message || '请求失败')
      throw new Error(body?.message)
    }
    // 处理业务 code 错误(后端约定 code=0 成功)
    if (response.headers.get('Content-Type')?.includes('application/json')) {
      const body = await response.clone().json()
      if (body?.code !== undefined && body.code !== 0 && body.code !== 200) {
        showError(body?.message || '业务错误')
        throw new Error(body?.message)
      }
    }
    return response
  }
}

第五步:创建 client

ts 复制代码
// src/api/client.ts
import createClient from 'openapi-fetch'
import type { paths } from './server-schema.ts'
import { middleware } from './middleware.ts'

export const apiClient = createClient<paths>({
  baseUrl: import.meta.env.VITE_API_BASE_URL
})

apiClient.use(middleware)

第六步:在业务代码中使用

ts 复制代码
// src/views/persona/index.vue
import { apiClient } from '@/api/client'

async function loadPersonaList() {
  const { data, error } = await apiClient.GET('/persona/config/list', {
    params: { query: { page: 1, size: 20 } }
  })
  if (error) return
  personaList.value = data.data.records  // 完整类型推断
}

async function savePersona(form: PersonaForm) {
  const { data } = await apiClient.POST('/persona/config/save', {
    body: form  // form 字段类型由 schema 约束
  })
  return data
}

团队协作中的价值

接入这套工具链后,团队的工作流变成:

复制代码
后端更新接口
    ↓
前端运行 pnpm gen:api
    ↓
TypeScript 编译报错,精确指出哪些调用处受到影响
    ↓
按报错修复,确保不遗漏

相比之前:

  • 不再需要对着 Swagger 文档手写类型,生成脚本一键同步
  • 接口变更有编译期保障,不会等到运行时才发现
  • IDE 自动补全接口路径和参数,减少查文档的时间
  • 新同学上手更快,不需要熟悉所有接口,IDE 会告诉你能传什么

常见问题

Q:server-schema.ts 可以手动修改吗?

不建议。文件头有 Do not make direct changes to the file. 的注释,每次重新生成会覆盖。如果后端文档有问题,应该推动后端修复,或者在生成后用脚本做后处理。

Q:多个后端服务怎么处理?

每个服务单独生成一个 server-schema.ts,创建各自的 createClient<paths>,然后分别导出:

ts 复制代码
export const serviceAClient = createClient<ServiceAPaths>({ baseUrl: '...' })
export const serviceBClient = createClient<ServiceBPaths>({ baseUrl: '...' })

Q:本地开发需要代理,baseUrl 怎么配?

配合 Vite 的 proxy 使用时,baseUrl 设为空字符串 '' 即可,请求路径保持原样由 proxy 转发:

ts 复制代码
export const apiClient = createClient<paths>({ baseUrl: '' })

Q:响应数据是包在 { code, data, msg } 外层的,怎么处理?

在 middleware 的 onResponse 里处理。如果想让 data 直接拿到内层数据,可以修改 response:

ts 复制代码
async onResponse({ response }) {
  const body = await response.clone().json()
  // 返回一个新的 Response,body 替换为内层 data
  return new Response(JSON.stringify(body.data), response)
}

但这样会丢失外层的 codemsg 类型信息,按项目需求取舍。


总结

这套工具链的本质是:把后端 API 的约定从运行时移到编译时

  • zx:让代码生成脚本写起来像 shell,维护成本接近零
  • swagger2openapi:消除 Swagger 2.0 和 OpenAPI 3.0 的格式鸿沟
  • openapi-typescript:把 API 文档"翻译"成 TypeScript 类型,一次生成,长期受益
  • openapi-fetch:让每一次 HTTP 调用都有类型保障,把接口错误拦截在编译阶段

四个工具各司其职,组合起来的效果远大于单独使用任何一个。

相关推荐
cypking2 小时前
二次封装ElementUI日期范围组件:打造带限制规则的Vue2 v-model响应式通用组件
前端·javascript·elementui
A923A2 小时前
【小兔鲜电商前台 | 项目笔记】第二天
前端·vue.js·笔记·项目·小兔鲜
牧码岛2 小时前
Web前端之样式中的light-dark函数,从媒体查询到颜色函数,从颜色到图片,light-dark打开CSS新时代、主题切换的暗黑模式到image的正解
前端·css·web·web前端
API快乐传递者2 小时前
从零构建高可用API接口:架构设计、性能优化与安全实践
安全·性能优化
酉鬼女又兒2 小时前
零基础快速入门前端蓝桥杯Web考点深度解析:var、let、const与事件绑定实战(可用于备赛蓝桥杯Web应用开发)
开发语言·前端·javascript·职场和发展·蓝桥杯·es6·html5
宁雨桥2 小时前
前端项目实现光暗主题切换的完整方案
前端
happymaker06262 小时前
vue指令扩展以及监视器的使用
前端·javascript·vue.js
一只小阿乐3 小时前
vue前端处理流式数据
前端·javascript·ai·大模型·全栈开发·agentai
问道飞鱼3 小时前
【技术方案】面向 Web 系统的《全栈灰度部署方案设计》
前端·全栈·灰度发布