Orval 是基于 OpenAPI Schema 自动生成 TypeScript API Client 的工具。
对于 Django + DRF + React 项目,Orval 可以自动生成:
- TypeScript 请求和响应类型。
- HTTP 请求函数。
- TanStack Query Hooks。
- Query Key 和 Query Options。
- Mutation Hooks 和 Mutation Options。
- 用于前端测试的 Mock 数据及 MSW Handler。
基本工作流程:
Django / DRF
↓
drf-spectacular
↓
OpenAPI Schema
↓
Orval
│
├── TypeScript Types
├── Request Functions
├── Query Keys
├── Query Options
├── Mutation Options
└── React Query Hooks
↓
React Components
Orval 的主要价值是减少手动维护 API 类型和请求代码的工作量,并使前后端接口保持一致。
1. 安装与基础配置
安装:
pnpm add -D orval
pnpm add axios @tanstack/react-query
通常在项目根目录创建:
orval.config.ts
基础配置
import { defineConfig } from "orval";
export default defineConfig({
api: {
input: {
target: "./openapi.yaml",
},
output: {
target: "./src/api/generated/index.ts",
schemas: "./src/api/generated/model",
client: "react-query",
httpClient: "axios",
mode: "tags-split",
},
},
});
主要配置:
| 配置 | 作用 |
|---|---|
input.target | OpenAPI 文件路径或 URL |
output.target | 生成的 API 代码输出位置 |
output.schemas | TypeScript Model 输出目录 |
output.client | 生成的客户端类型 |
output.httpClient | HTTP 请求实现 |
output.mode | 生成文件的组织方式 |
output.mock | 是否生成 Mock |
client 与 httpClient 的区别
client: "react-query",
httpClient: "axios",
含义:
react-query:生成 TanStack Query Hooks 和辅助函数。axios:请求函数使用 Axios 发送 HTTP 请求。
两者解决的是不同问题。
2. 生成命令
pnpm exec orval
可以加入 package.json:
{
"scripts": {
"api:generate": "orval",
"build": "tsc -b && vite build"
}
}
执行:
pnpm api:generate
Orval 会读取 OpenAPI Schema,生成对应的 TypeScript 代码。
通常不建议直接编辑生成文件,因为重新生成时修改会被覆盖。
3. operationId
OpenAPI 中的 operationId 通常决定生成函数的名称。
例如:
paths:
/api/v1/user/profile/:
get:
operationId: userProfileRetrieve
responses:
"200":
description: Success
可能生成:
userProfileRetrieve();
useUserProfileRetrieve();
getUserProfileRetrieveQueryKey();
getUserProfileRetrieveQueryOptions();
具体函数名称和可用辅助函数取决于 Orval 版本与配置。
命名原则
建议在 Django 中使用稳定的 operationId:
@extend_schema(
operation_id="getUserProfile",
)
这样可能生成:
getUserProfile();
useGetUserProfile();
getGetUserProfileQueryKey();
需要注意:修改 operationId 可能改变所有相关生成函数名称,进而导致前端 TypeScript 编译错误。
因此,operationId 应被视为 API Client 的一部分公共契约。
4. 普通 Request Function
假设后端接口:
GET /api/v1/user/profile/
Orval 可以生成类似:
export const userProfileRetrieve = (
signal?: AbortSignal,
): Promise<UserProfile> => {
return axiosInstance<UserProfile>({
url: "/api/v1/user/profile/",
method: "GET",
signal,
});
};
这里假设 axiosInstance<T> 是自定义 Mutator,并且返回 Promise<T>,其中 T 是响应体类型。
请求流程:
React Component
↓
Request Function
↓
Axios Instance
↓
HTTP Request
↓
Django API
Request Function 的职责
- 构建 API URL。
- 指定 HTTP Method。
- 传递 Query Parameters。
- 传递 Request Body。
- 指定 TypeScript 返回类型。
- 调用 HTTP Client。
它本身通常不负责 TanStack Query 缓存。
5. TanStack Query 集成
配置:
client: "react-query",
Orval 会生成用于 React 的 TanStack Query 代码。
其主要结构是:
Request Function
│
▼
QueryFn
│
├── Query Key
│
└── Query Options
│
▼
useQuery
│
▼
React Component
注意:queryKey、queryFn 和 queryOptions 是共同组成 Query 的配置关系,不是依次执行的请求步骤。
6. Query Key
Query Key 是 TanStack Query 用来标识缓存数据的唯一键。
Orval 通常会生成 Query Key 工厂:
export const getUserProfileRetrieveQueryKey = () => {
return ["/api/v1/user/profile/"] as const;
};
对于带参数的接口:
GET /api/v1/pois?city=1&page=2
Query Key 应区分不同参数。
例如:
["/api/v1/pois", { city: 1, page: 2 }];
与:
["/api/v1/pois", { city: 2, page: 2 }];
应该是不同的缓存条目。
Query Key 的主要用途
- 标识缓存。
- 读取指定缓存。
- 更新指定缓存。
- 删除缓存。
- 使缓存失效。
- 控制查询重新获取。
例如:
queryClient.invalidateQueries({
queryKey: getUserProfileRetrieveQueryKey(),
});
这会将匹配的 Query 标记为 stale,并在默认配置下重新获取活跃 Query 的数据。
7. Query Options
Orval 可能生成:
export const getUserProfileRetrieveQueryOptions = () => {
return {
queryKey: getUserProfileRetrieveQueryKey(),
queryFn: ({ signal }: { signal?: AbortSignal }) =>
userProfileRetrieve(signal),
};
};
实际生成代码还会包含复杂的 TypeScript 泛型、参数和重载。
Query Options 的作用
将 TanStack Query 的配置集中在一个对象中:
{
queryKey,
queryFn,
staleTime,
gcTime,
enabled,
retry,
refetchOnWindowFocus,
}
例如在 React 中:
const options = getUserProfileRetrieveQueryOptions();
const result = useQuery({
...options,
staleTime: 60_000,
});
staleTime 表示数据被视为新鲜的时间。
gcTime 表示不活跃 Query 的缓存可以保留多久。
注意:这两个参数由 TanStack Query 实现,Orval 只是生成相应的配置代码。
8. useQuery Hook
Orval 可以进一步生成:
export const useUserProfileRetrieve = () => {
return useQuery(getUserProfileRetrieveQueryOptions());
};
实际代码通常需要支持额外的 Query Options 和 Axios Options。
使用
function UserProfile() {
const { data, isPending, isError, error } = useUserProfileRetrieve();
if (isPending) {
return <div>Loading...</div>;
}
if (isError) {
return <div>{error.message}</div>;
}
return <div>{data.name}</div>;
}
TanStack Query 负责管理:
data:获取的数据。isPending:是否处于等待初始数据的状态。isFetching:是否正在执行请求。isError:是否发生错误。error:错误对象。refetch:手动重新请求。
手动 Query 与 Orval Hook
不使用 Orval:
useQuery({
queryKey: ["user", "profile"],
queryFn: getUserProfile,
});
使用 Orval:
useUserProfileRetrieve();
后者减少了重复编写请求函数、Query Key 和类型定义的工作量。
9. Mutation
Mutation 通常用于执行具有副作用的操作,例如:
- POST:创建。
- PUT:替换。
- PATCH:修改。
- DELETE:删除。
假设:
PATCH /api/v1/user/profile/
Orval 可能生成以下代码。
Request Function
export const userProfilePartialUpdate = (
data: PatchedUserProfile,
): Promise<UserProfile> => {
return axiosInstance<UserProfile>({
url: "/api/v1/user/profile/",
method: "PATCH",
data,
});
};
Mutation Options
export const getUserProfilePartialUpdateMutationOptions = () => {
return {
mutationFn: (variables: { data: PatchedUserProfile }) =>
userProfilePartialUpdate(variables.data),
};
};
Mutation Hook
export const useUserProfilePartialUpdate = () => {
return useMutation(getUserProfilePartialUpdateMutationOptions());
};
整体关系:
useMutation
↓
Mutation Options
↓
mutationFn
↓
Request Function
↓
Axios
↓
Django API
10. 使用 Mutation
function ProfileEditor() {
const mutation = useUserProfilePartialUpdate();
const handleUpdate = () => {
mutation.mutate({
data: {
name: "Alice",
},
});
};
return (
<button onClick={handleUpdate} disabled={mutation.isPending}>
Update
</button>
);
}
mutate 与 mutateAsync
mutation.mutate({
data: { name: "Alice" },
});
mutate() 用于触发 Mutation,通常通过回调处理结果。
而:
await mutation.mutateAsync({
data: { name: "Alice" },
});
mutateAsync() 返回 Promise,适合需要等待操作完成的场景。
Mutation 状态
| 属性 | 含义 |
|---|---|
isIdle | 尚未执行 |
isPending | 正在执行 |
isSuccess | 成功 |
isError | 失败 |
data | 成功返回的数据 |
error | 错误信息 |
reset() | 重置 Mutation 状态 |
11. Mutation 与缓存失效
Mutation 修改服务器数据后,已经缓存的 Query 数据可能过期。
例如:
GET User Profile
↓
Cache User Data
↓
PATCH User Profile
↓
Server Data Changed
↓
Cached Data Outdated
因此通常需要在 Mutation 成功后,使相关 Query 失效。
invalidateQueries
const queryClient = useQueryClient();
const mutation = useUserProfilePartialUpdate({
mutation: {
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: getUserProfileRetrieveQueryKey(),
});
},
},
});
这里展示的是常见的 Orval Hook 配置方式,具体参数结构需要以生成文件为准。
为什么需要 invalidateQueries?
因为 Mutation 和 Query 的缓存是独立管理的。
执行:
useUserProfilePartialUpdate();
并不必然意味着:
useUserProfileRetrieve();
的缓存会自动更新。
Orval 支持配置生成与缓存失效相关的行为,但是否自动失效取决于配置,不能默认假设所有 Mutation 都会自动更新相关 Query。
12. setQueryData
除了重新请求,也可以直接修改缓存。
queryClient.setQueryData(getUserProfileRetrieveQueryKey(), updatedProfile);
适合 Mutation 已经返回最新完整资源的情况。
invalidateQueries 与 setQueryData
| 方法 | 作用 |
|---|---|
invalidateQueries | 标记缓存失效,并按条件重新获取 |
setQueryData | 直接写入缓存 |
removeQueries | 删除缓存条目 |
resetQueries | 重置 Query 状态 |
推荐原则:
- Mutation 只返回成功状态:通常使用
invalidateQueries。 - Mutation 返回完整且可信的最新数据:可以考虑
setQueryData。 - 多个查询依赖同一资源:需要同时考虑相关缓存的一致性。
13. Axios Mutator
Mutator 是 Orval 非常重要的扩展机制。
它允许使用项目自己的 Axios 实例,而不是让每个生成函数直接调用默认 Axios。
orval.config.ts
export default defineConfig({
api: {
input: {
target: "./openapi.yaml",
},
output: {
target: "./src/api/generated/index.ts",
client: "react-query",
httpClient: "axios",
override: {
mutator: {
path: "./src/api/axios-instance.ts",
name: "axiosInstance",
},
},
},
},
});
自定义 Axios Instance
import Axios, { type AxiosRequestConfig, type AxiosError } from "axios";
const client = Axios.create({
baseURL: "/",
withCredentials: true,
timeout: 30_000,
});
export const axiosInstance = <T>(
config: AxiosRequestConfig,
options?: AxiosRequestConfig,
): Promise<T> => {
return client({
...config,
...options,
}).then(({ data }) => data);
};
export type ErrorType<E> = AxiosError<E>;
export type BodyType<T> = T;
这里的 withCredentials: true 对跨源 Cookie 请求有用,但并不自动实现 CSRF 防护。
实际调用:
Orval Generated Function
↓
axiosInstance()
↓
Axios Client
↓
Request Interceptor
↓
Django API
↓
Response Interceptor
↓
Response Data
Mutator 的用途
- 设置 Base URL。
- 携带 Cookie。
- 添加 Authorization Header。
- 处理 CSRF。
- 统一超时设置。
- 处理 API 错误。
- 转换响应数据。
- 处理请求取消。
14. Axios Interceptors
可以在自定义 Axios Instance 中配置拦截器。
Request Interceptor
client.interceptors.request.use((config) => {
config.headers.set("X-App-Version", "1.0.0");
return config;
});
可以用于添加公共 Header。
Response Interceptor
client.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Handle authentication error
}
return Promise.reject(error);
},
);
注意:401 不应该一律触发跳转登录页,例如某些接口可能允许匿名访问,或者正在执行登录验证。
全局错误处理需要考虑业务上下文。
15. AbortSignal:取消请求
Orval 生成的 Query Function 通常可以将 TanStack Query 提供的 AbortSignal 传递给 Axios。
例如:
queryFn: ({ signal }) => userProfileRetrieve(signal);
请求函数:
axiosInstance({
url: "/api/v1/user/profile/",
method: "GET",
signal,
});
当 TanStack Query 取消请求时,Axios 可以使用这个 Signal 中止对应的 HTTP 请求。
常见场景:
- 用户离开页面。
- 查询条件变化。
- 手动取消 Query。
- 请求结果不再需要。
需要注意:浏览器中止 HTTP 请求,并不保证服务器已经停止处理请求。
16. TypeScript 类型生成
Orval 会根据 OpenAPI Schema 生成 TypeScript 类型。
例如:
UserProfile:
type: object
required:
- id
- name
properties:
id:
type: integer
name:
type: string
avatar:
type: string
nullable: true
可能生成:
export interface UserProfile {
id: number;
name: string;
avatar?: string | null;
}
required 与 nullable
两者不同:
| Schema | TypeScript 含义 |
|---|---|
| required | 属性必须存在 |
| 非 required | 属性可以省略 |
| nullable | 属性值可以是 null |
例如:
name: string;
表示必填,且不允许 null。
name?: string | null;
表示属性可以省略,也允许 null。
类型生成的局限
Orval 根据 Schema 生成静态类型,并不自动验证运行时数据。
例如:
const profile: UserProfile = await userProfileRetrieve();
TypeScript 会相信 API 函数声明的返回类型,但如果服务器实际返回了错误的数据结构,TypeScript 本身无法在运行时发现。
需要运行时校验时,可以使用 Zod 等工具,或依赖后端 Serializer 保证输出结构。
17. Output Mode
Orval 支持不同的生成文件组织方式。
常见模式:
| Mode | 作用 |
|---|---|
single | 将生成内容集中在一个文件 |
split | 分离 API 与 Schema |
tags | 根据 Tags 分组 |
tags-split | 根据 Tags 分组并拆分文件 |
例如:
output: {
mode: "tags-split",
target: "./src/api/generated/index.ts",
schemas: "./src/api/generated/model",
}
可以生成类似:
src/api/generated/
├── users/
│ └── users.ts
├── pois/
│ └── pois.ts
├── itineraries/
│ └── itineraries.ts
└── model/
├── userProfile.ts
├── poi.ts
└── itinerary.ts
实际目录和文件名由配置与 Schema Tags 决定。
对于接口数量较多的项目,tags-split 通常更有利于按业务模块组织代码。
18. Mock 与 MSW
Orval 还可以根据 OpenAPI Schema 生成 Mock。
配置:
output: {
client: "react-query",
mock: true,
}
Mock 可以配合 MSW(Mock Service Worker)拦截 HTTP 请求,返回模拟数据。
基本过程:
React Component
↓
Generated API Function
↓
HTTP Request
↓
MSW Interceptor
↓
Mock Response
适合:
- 后端接口尚未开发完成。
- 前端独立开发。
- UI 测试。
- 模拟不同 API 返回结果。
- 测试错误状态和空数据。
注意:Mock 只能模拟接口行为,不能证明真实后端与 OpenAPI 契约完全一致。
19. 生成代码与手写代码的职责边界
这是使用 Orval 时很重要的工程原则。
Generated Code
由 Orval 自动维护:
generated/
├── types
├── request functions
├── query hooks
├── mutation hooks
└── query key factories
不建议手动修改。
Handwritten Code
由开发者维护:
api/
├── axios-instance.ts
├── csrf.ts
├── query-client.ts
├── generated/
└── services/
主要负责:
- Axios Instance 和 Interceptors。
- CSRF 处理。
- QueryClient 配置。
- 业务级缓存更新。
- 多接口组合。
- 特殊错误处理。
推荐原则
Orval 负责标准化的接口代码,业务流程和跨接口逻辑由开发者维护。
不要将复杂业务逻辑写入生成文件。
20. Orval 与 React Hook Form
对于表单提交,可以结合:
React Hook Form
↓
Form Validation
↓
handleSubmit()
↓
Orval Mutation Hook
↓
Axios
↓
Django API
例如:
type FormValues = {
name: string;
};
function ProfileForm() {
const { register, handleSubmit } = useForm<FormValues>();
const mutation = useUserProfilePartialUpdate();
const onSubmit = (values: FormValues) => {
mutation.mutate({
data: values,
});
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name")} />
<button type="submit" disabled={mutation.isPending}>
Save
</button>
</form>
);
}
React Hook Form 负责表单状态和验证,Orval 生成的 Mutation Hook 负责调用 API。
二者职责明确,可以直接组合使用。
21. Orval 与 CI/CD
推荐将 Schema 生成和客户端代码生成纳入 CI。
典型流程:
Django API Changes
↓
pytest
↓
drf-spectacular
↓
openapi.yaml
↓
Orval
↓
Generated TypeScript
↓
TypeScript Check
↓
Vite Build
例如:
# Django Backend
python manage.py spectacular \
--file openapi.yaml \
--validate \
--fail-on-warn
# React Frontend
pnpm api:generate
# TypeScript
pnpm exec tsc -b
# Build
pnpm build
这样可以在构建阶段发现部分接口契约不一致问题。
例如:
Backend changes field name
↓
OpenAPI Schema updated
↓
Orval regenerates types
↓
Existing frontend code fails typecheck
↓
CI detects incompatibility
需要注意:TypeScript 检查只能检测能够被静态类型系统发现的问题,不能保证后端运行时一定遵守 Schema。
22. 常见问题
Q1:Orval 是否替代 Axios?
不是。
Orval 是代码生成工具,Axios 是 HTTP Client。
Orval 可以生成使用 Axios 的请求函数,也可以配置其他 HTTP Client。
Q2:Orval 是否替代 TanStack Query?
不是。
Orval 可以生成 TanStack Query Hooks。
但缓存、请求状态、重试、失效和垃圾回收仍然由 TanStack Query 实现。
Q3:是否需要手动维护 Query Key?
通常不需要。
Orval 可以根据接口参数生成 Query Key。
但复杂业务中的缓存组织、跨 Query 失效策略仍需要开发者设计。
Q4:Mutation 成功后是否自动刷新 Query?
不能默认认为会。
可以在 onSuccess 中使用:
queryClient.invalidateQueries(...)
或者根据 Orval 版本与配置启用生成式缓存失效机制。
Q5:为什么生成代码包含大量泛型和重载?
主要为了支持:
- 自定义响应类型。
- 不同 Query Options。
initialData类型推断。- 错误类型推断。
- Mutation Variables 类型。
- Axios 请求选项。
- TanStack Query 的类型系统。
日常使用通常不需要逐行理解全部生成代码。
重点是了解 Request Function、Query Key、Query Options、Mutation Options 和 Hook 之间的关系。
总结
Orval 的核心功能可以分为四个层次:
| 层次 | 核心内容 |
|---|---|
| 代码生成 | OpenAPI、operationId、TypeScript Types |
| HTTP Client | Request Function、Axios、Mutator |
| TanStack Query | Query Key、Query Options、useQuery、useMutation |
| 工程实践 | 缓存失效、Mock、CI/CD、生成代码管理 |
最重要的整体架构:
Backend
Django / DRF
↓
drf-spectacular
↓
OpenAPI Schema
↓
Orval
↓
Frontend Generated Code
│
├── Types
├── Request Functions
├── Query Hooks
└── Mutation Hooks
↓
TanStack Query
↓
React UI
Orval 负责生成接口契约对应的代码,Axios 负责 HTTP 通信,TanStack Query 负责服务器状态管理,React 负责 UI。