← 编程返回顶部

Orval:OpenAPI TypeScript Client 代码生成

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.targetOpenAPI 文件路径或 URL
output.target生成的 API 代码输出位置
output.schemasTypeScript Model 输出目录
output.client生成的客户端类型
output.httpClientHTTP 请求实现
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

两者不同:

SchemaTypeScript 含义
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 ClientRequest Function、Axios、Mutator
TanStack QueryQuery 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。


参考文档