使用 Orval 8 从 OpenAPI 自动生成类型安全的 TypeScript API 客户端

前后端分离项目里,接口字段通常会被维护两遍:后端维护 OpenAPI 文档,前端再手写请求函数和 TypeScript 类型。只要接口发生变化,两边就很容易出现字段名、可空性或响应类型不一致的问题。

Orval 做的事情,是把 OpenAPI 规范直接转换成前端可调用的 TypeScript 代码。它不只是“生成接口类型”,还可以生成 Axios/Fetch 请求函数、TanStack Query Hooks、MSW Mock、Faker 数据工厂以及 Zod Schema。

本文基于 Orval 8.x 编写。写作时 npm 的 latest 版本为 8.28.1。Orval v8 要求 Node.js 22.18.0 或更高版本。

一、Orval 需要什么样的接口文档

Orval 支持以下输入:

  • OpenAPI v3 或 Swagger v2;
  • JSON 或 YAML 格式;
  • 本地文件、远程 URL,也可以直接传入 JavaScript 对象;
  • 多个候选地址,Orval 会依次尝试并使用第一个可访问的文档。

例如,Springdoc 项目常见的文档地址是:

http://localhost:8080/v3/api-docs

如果后端配置了分组,也可能是:

http://localhost:8080/v3/api-docs/{group}

这里需要的是原始 OpenAPI JSON/YAML,而不是 Swagger UI 的 HTML 页面。可以先在浏览器中打开地址,确认返回的是包含 openapi(Swagger v2 中是 swagger)、info、paths 等字段的文档。

另外,Orval 生成的是 TypeScript 代码,因此项目需要能够编译 TypeScript。它并不要求你使用某个特定前端框架。

二、安装 Orval 和 Axios

本文使用 pnpm,并通过自定义 Axios 实例统一处理基础地址、鉴权和错误:

pnpm add -D orval
pnpm add axios

使用 npm 或 Yarn 时,对应命令如下:

npm install orval -D
npm install axios
yarn add -D orval
yarn add axios

安装完成后可以检查版本:

pnpm orval --version

如果现有项目的 Node.js 版本低于 22.18.0,需要先升级 Node.js,或者按照官方安装文档使用 Docker 运行 Orval 8。

三、规划目录

建议把生成代码和手写代码彻底分开:

src/api/
├── generated/       # Orval 生成,禁止手动修改
│   ├── endpoints/
│   └── model/
└── http/
    └── client.ts    # 手写的 Axios 实例

这样做很重要,因为后面会开启 clean: true。Orval 每次生成前会清理自己管理的 target 和 schemas 目录;如果把手写文件放进去,也会一起被删除。

四、创建 Orval 配置

在项目根目录创建 orval.config.ts:

import { defineConfig } from 'orval';

export default defineConfig({
  backend: {
    input: {
      // 可以替换成本地文件,例如 './openapi.yaml'
      target: 'http://localhost:8080/v3/api-docs',
    },
    output: {
      target: './src/api/generated/endpoints',
      schemas: './src/api/generated/model',
      mode: 'tags-split',
      client: 'axios-functions',
      clean: true,
      override: {
        mutator: {
          path: './src/api/http/client.ts',
          name: 'request',
        },
      },
    },
    hooks: {
      afterAllFilesWrite: 'prettier --write',
    },
  },
});

这份配置中最关键的字段如下:

配置 作用
input.target OpenAPI 文件路径或 URL
output.target 请求函数的输出位置
output.schemas TypeScript 数据模型的输出位置
mode: 'tags-split' 按 OpenAPI tags 建目录,并拆分接口文件
client: 'axios-functions' 生成可直接导入的 Axios 请求函数
clean: true 生成前清理旧文件,避免接口删除后残留无效代码
override.mutator 让生成代码通过我们自己的请求函数发送请求
afterAllFilesWrite 生成完成后格式化本次生成的文件

afterAllFilesWrite 会把生成的文件路径追加到命令后面,所以写成 prettier --write 即可。如果项目没有使用 Prettier,可以删除这个 Hook,或者替换成项目已有的格式化命令。

mode 还支持 single、split 和 tags。接口较多时,tags-split 通常更容易维护。不过这也意味着后端的 tags 会直接影响目录结构,后面会专门讨论这一点。

需要特别区分 client 和 httpClient:client 决定生成哪一类代码,例如普通请求函数或 React Query Hooks;httpClient 决定 React Query、SWR 等兼容客户端底层使用 Fetch 还是 Axios。Orval v8 的 httpClient 默认为 fetch,而本文通过 client: 'axios-functions' 明确生成 Axios 函数。

如果项目不需要 Axios 拦截器,也可以把 client 改成 fetch,同时删除 Axios 依赖和 mutator 配置,直接生成基于原生 Fetch 的请求函数。

五、编写自定义 Axios 实例

创建 src/api/http/client.ts:

import Axios, {
  type AxiosError,
  type AxiosRequestConfig,
} from 'axios';

export const AXIOS_INSTANCE = Axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10_000,
});

AXIOS_INSTANCE.interceptors.request.use((config) => {
  const token = localStorage.getItem('access_token');

  if (token) {
    config.headers.set('Authorization', `Bearer ${token}`);
  }

  return config;
});

AXIOS_INSTANCE.interceptors.response.use(
  (response) => response,
  (error) => {
    // 可以在这里统一处理 401、错误提示或日志上报
    return Promise.reject(error);
  },
);

export const request = <T>(
  config: AxiosRequestConfig,
  options?: AxiosRequestConfig,
): Promise<T> => {
  return AXIOS_INSTANCE({
    ...config,
    ...options,
    headers: {
      ...config.headers,
      ...options?.headers,
    },
  }).then(({ data }) => data);
};

export type ErrorType<Error> = AxiosError<Error>;
export type BodyType<BodyData> = BodyData;

这里有几个容易被忽略的细节:

  1. request<T> 返回的是 Promise<T>,也就是响应体本身,而不是 AxiosResponse<T>。
  2. 响应拦截器仍然返回完整的 response,最后统一通过 .then(({ data }) => data) 取出数据。这样类型与运行时行为是一致的,不需要使用 as unknown as T 强行断言。
  3. 第二个 options 参数允许调用方为单次请求覆盖 Axios 配置,例如传入 signal、自定义请求头或超时时间。
  4. ErrorType 和 BodyType 会被 React Query、SWR 等生成器用于推导错误与请求体类型。

上面的 Token 读取方式适用于纯浏览器 Vite 项目。如果项目使用 SSR,请求拦截器也可能在 Node.js 中执行,不能直接访问 localStorage;应增加运行环境判断,或者从 Cookie、请求上下文、依赖注入中获取 Token。

还要注意,OpenAPI 文档中的 servers 和运行时请求地址并不是一回事。本文通过 VITE_API_BASE_URL 控制实际请求地址,可以分别在 .env.development 和 .env.production 中配置:

# .env.development
VITE_API_BASE_URL=http://localhost:8080
# .env.production
VITE_API_BASE_URL=https://api.example.com

六、添加生成命令

在 package.json 中添加脚本:

{
  "scripts": {
    "api:generate": "orval --config ./orval.config.ts",
    "api:watch": "orval --config ./orval.config.ts --watch"
  }
}

执行一次生成:

pnpm api:generate

当 input.target 是本地 OpenAPI 文件时,开发期间还可以监听文件变化:

pnpm api:watch

--watch 监听的是本地文件变化,不会持续轮询远程 OpenAPI URL。使用远程文档时,应在后端文档更新后重新执行 pnpm api:generate。

如果配置文件中定义了多个项目,可以通过 --project 只生成其中一个:

pnpm orval --project backend

使用 tags-split 后,生成结果大致如下:

src/api/generated/
├── endpoints/
│   ├── user/
│   │   └── user.ts
│   └── order/
│       └── order.ts
└── model/
    ├── index.ts
    ├── user.ts
    └── order.ts

实际文件名和函数名取决于 OpenAPI 中的 tags 与 operationId。

七、在业务代码中使用生成的接口

假设后端为查询用户接口定义了 operationId: getUserById,Orval 会生成同名或经过规范化的请求函数:

import { getUserById } from '@/api/generated/endpoints/user/user';

const user = await getUserById('42');

console.log(user.name);

模型也可以单独导入:

import type { User } from '@/api/generated/model';

const printUser = (user: User) => {
  console.log(user.name);
};

接口参数、请求体和响应值都会从 OpenAPI Schema 中推导。后端修改接口后,重新运行 pnpm api:generate,TypeScript 就能帮助我们定位受影响的前端代码。

不要直接修改 generated 目录中的文件。需要改变请求行为时修改 client.ts;需要改变生成结果时修改 OpenAPI 文档或 orval.config.ts。

八、生成 TanStack Query Hooks

如果项目使用 TanStack Query,只需要把客户端改成 react-query:

export default defineConfig({
  backend: {
    input: {
      target: 'http://localhost:8080/v3/api-docs',
    },
    output: {
      target: './src/api/generated/endpoints',
      schemas: './src/api/generated/model',
      mode: 'tags-split',
      client: 'react-query',
      httpClient: 'axios',
      clean: true,
      override: {
        mutator: {
          path: './src/api/http/client.ts',
          name: 'request',
        },
      },
    },
  },
});

并安装运行时依赖:

pnpm add @tanstack/react-query axios

生成结果会包含 Query/Mutation Hooks、Query Key 和 Options 工厂。Orval v8 将兼容客户端的默认 HTTP transport 从 Axios 改成了 Fetch,因此想继续复用 Axios mutator 时,最好显式设置 httpClient: 'axios'。

九、生成 MSW Mock

Orval 8 可以同时生成 MSW Handler 和 Faker 数据工厂。需要 Mock 时,在 output 中添加:

mock: {
  generators: [
    {
      type: 'msw',
      delay: 300,
      useExamples: true,
    },
  ],
},

同时安装 MSW:

pnpm add -D msw

useExamples: true 会优先使用 OpenAPI 中提供的 example。这也是为什么后端维护高质量示例数据很有价值。

Orval v8 中,mock: true 的含义已经发生变化:它会同时生成 .msw.ts 和 .faker.ts 文件。如果只想生成 MSW,请使用上面的 generators 显式配置,不要只写 mock: true;如果启用 Faker 数据工厂,还需要安装 @faker-js/faker。

十、Orval 8 的几个重要变化

如果项目是从旧版本升级,需要重点检查以下内容:

  1. Orval 已迁移到 ESM,并要求 Node.js 22.18.0 或更高版本。
  2. 兼容客户端的默认 HTTP transport 从 Axios 改成了 Fetch。依赖 Axios 行为时应显式配置。
  3. input.validation 已移除。v8 会使用更严格的解析器校验 OpenAPI 文档。
  4. mock 改为基于 generators 数组的配置;mock: true 现在同时启用 MSW 和 Faker。
  5. override.fetch.explode、override.coerceTypes 和 override.useNativeEnums 等旧选项已经移除。
  6. Zod Schema 名称改为 PascalCase,依赖旧生成名称的导入需要同步调整。

遇到旧文档中的配置无法使用时,先对照官方的 Upgrading to v8 页面,而不是通过关闭类型检查来绕过问题。

十一、常见问题与实践建议

1. OpenAPI 校验失败

Orval v8 的校验比旧版本严格。优先修复后端生成的 OpenAPI 文档;确实需要在生成前调整文档时,可以使用 input.override.transformer。unsafeDisableValidation 只应该作为最后手段,因为关闭校验可能把错误继续传递到生成代码中。

2. 开启 clean 后手写文件消失

clean: true 会清理 target 和 schemas。生成目录中只能放生成文件,自定义 Axios、Mock Server 和业务封装都应该放在目录外。

3. 后端 Tag 能不能使用中文

可以,但不建议把面向用户展示的中文描述直接作为代码分组标识。tags-split 会根据 Tag 生成目录和文件名,中文、空格以及频繁改名都会让导入路径不稳定。更稳妥的做法是:

  • tags 使用稳定、简短的英文标识,例如 user、order;
  • 接口说明使用 summary 和 description;
  • 每个接口提供唯一且稳定的 operationId,例如 getUserById。

这不是 Orval 对中文的硬性限制,而是为了让生成的代码结构和 Git Diff 更可控。

4. 团队是否应该提交生成代码

两种方式都可以,但团队必须统一:

  • 提交生成代码:代码审查时可以直接看到接口变化,但 Diff 会更多;
  • 不提交生成代码:仓库更干净,但本地开发和 CI 都必须先执行生成命令。

如果选择提交生成代码,可以在 CI 中检查它是否为最新:

pnpm api:generate
git diff --exit-code -- src/api/generated

这样后端文档变化后,如果开发者忘记重新生成,CI 会直接失败。

总结

Orval 的价值不是少写几个 Axios 函数,而是让 OpenAPI 成为前后端共享的接口契约:

  1. 后端维护 OpenAPI Schema、operationId、tags 和示例;
  2. Orval 生成请求函数、类型、Hooks 或 Mock;
  3. 前端只维护 Axios/Fetch 基础设施和业务逻辑;
  4. TypeScript 与 CI 负责暴露接口变更带来的影响。

当接口数量越来越多时,这套流程能明显减少重复代码,也能把很多联调阶段才会发现的问题提前到代码生成和编译阶段。

参考资料