使用Orval自动生成前端TypeScript接口文档
使用 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.js22.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;
这里有几个容易被忽略的细节:
request<T>返回的是Promise<T>,也就是响应体本身,而不是AxiosResponse<T>。- 响应拦截器仍然返回完整的
response,最后统一通过.then(({ data }) => data)取出数据。这样类型与运行时行为是一致的,不需要使用as unknown as T强行断言。 - 第二个
options参数允许调用方为单次请求覆盖 Axios 配置,例如传入signal、自定义请求头或超时时间。 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 的几个重要变化
如果项目是从旧版本升级,需要重点检查以下内容:
- Orval 已迁移到 ESM,并要求 Node.js
22.18.0或更高版本。 - 兼容客户端的默认 HTTP transport 从 Axios 改成了 Fetch。依赖 Axios 行为时应显式配置。
input.validation已移除。v8 会使用更严格的解析器校验 OpenAPI 文档。mock改为基于generators数组的配置;mock: true现在同时启用 MSW 和 Faker。override.fetch.explode、override.coerceTypes和override.useNativeEnums等旧选项已经移除。- 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 成为前后端共享的接口契约:
- 后端维护 OpenAPI Schema、
operationId、tags和示例; - Orval 生成请求函数、类型、Hooks 或 Mock;
- 前端只维护 Axios/Fetch 基础设施和业务逻辑;
- TypeScript 与 CI 负责暴露接口变更带来的影响。
当接口数量越来越多时,这套流程能明显减少重复代码,也能把很多联调阶段才会发现的问题提前到代码生成和编译阶段。