↓ 跳过正文

API Development Orval OpenAPI

Orval:从OpenAPI规范自动生成带类型的API客户端和mock

Orval读取OpenAPI规范,生成带类型的TypeScript客户端、React Query hooks和MSW mock,没人需要再手写API层。本文讲怎么配置、mock怎么用,以及它和openapi-generator、openapi-typescript、Hey API比起来怎么样。

Orval:从OpenAPI规范自动生成带类型的API客户端和mock
OpenApi - 这篇文章属于一个选集。
§ 2: 本文

Orval是一个代码生成器:它读取OpenAPI(Swagger)规范,替你写好前端的API层。每个接口一个带类型的函数或React Query hook,TypeScript模型和契约完全一致,需要的话还有填好Faker数据的MSW mock handler。你只要提交一个配置文件,运行orval,规范一变就再运行一次。

这个系列的第一篇里,我主张在写任何代码之前先把OpenAPI契约定下来。这一篇要讲的,是让这份契约真正“回本”的工具。

手写前端的API层,是那种没人会承认自己喜欢的活儿。从后端文档里抄类型,第四十次写同一个请求封装,三周后有人把某个字段改了名,一半的调用就悄无声息地坏掉了。Orval直接把这个活儿删掉了。把它指向你的OpenAPI规范,客户端代码就自动生成:每个接口都有带类型的函数,模型和契约完全一致,还有后端根本不存在时就能用来开发的mock。

前端团队为什么喜欢它
#

mock生成是最被低估的部分。规范一旦谈定,前端就能起一个假服务器,对着它把整个UI做完,不用再等那些永远“快好了”的接口。等真正的后端上线,把客户端指过去,大部分东西直接就能跑,因为两边是照着同一份文档做的。

另一个收获是:无聊的代码从此“天生正确”。没有人会在手写的请求体里敲错字段名了,因为根本没有人再手写请求体。

后端团队为什么也喜欢它
#

规范成为唯一事实来源之后,日常沟通都变了。不再有“这个接口到底返回什么来着?”的群聊追问,前端直接看生成的类型就行。后端开发者被打断得更少,反馈来得更早:前端对着mock开发时觉得哪个API别扭,你在改起来还便宜的阶段就知道了,而不是等它进了生产环境。

怎么融入工作流
#

没什么花哨的。后端提出规范,两个团队一起评审;前端用Orval生成客户端和mock,直接开工;后端按同一份契约实现真实接口。规范一变,两边重新生成,编译器会准确告诉你哪里坏了。最后这一点才是全部意义所在:前后端之间的偏差,从生产事故变成了编译错误。

配置
#

先把它装成开发依赖,再装上生成的代码要import的那些包。要生成React Query hooks和mock的话,就是TanStack Query、MSW和Faker:

npm install --save-dev orval msw @faker-js/faker
npm install @tanstack/react-query

然后在项目根目录加一个orval.config.ts。下面这份和Orval官方文档里的示例很接近:

import { defineConfig } from 'orval';

export default defineConfig({
  petstore: {
    input: {
      target: './openapi.yaml', // a path or a URL
    },
    output: {
      mode: 'single',
      target: './src/api/petstore.ts',
      schemas: './src/api/model',
      client: 'react-query',
      httpClient: 'fetch',
      mock: true, // MSW handlers plus Faker data
      override: {
        fetch: { includeHttpResponseReturnType: false },
      },
    },
  },
});

client决定你拿到什么:普通的fetch或axios函数,React Query、Vue Query、Svelte Query或SWR的hooks,Angular service,或者Zod schema。includeHttpResponseReturnType: false这一行让fetch客户端直接返回响应体,而不是{ data, status },组件要的通常就是这个。

再加一条script,省得每个人都得记命令:

{
  "scripts": {
    "api": "orval"
  }
}

npm run api会生成hooks和模型。组件里用起来和别的React Query hook没什么两样,类型都已经填好了:

import { useListPets } from './api/petstore';

export function PetList() {
  const { data: pets, isLoading } = useListPets();
  if (isLoading) return <p>Loading…</p>;
  return (
    <ul>
      {pets?.map((pet) => <li key={pet.id}>{pet.name}</li>)}
    </ul>
  );
}

在规范里给某个字段改个名,再跑一次npm run api,所有用到pet.name的地方都会变红。这就是上面说的偏差检查,而且跑一次不花任何成本。

对着mock开发
#

开了mock: true之后,Orval还会为每个接口生成一个MSW请求handler,返回按响应结构填充的Faker数据,外加一个把所有handler一次性返回的函数。把这份列表交给浏览器里MSW的setupWorker,或者测试里的setupServer,应用就跑在一个已经符合契约的假后端上了。每个handler还能传入覆盖值,测试想返回特定的payload、空列表或错误状态都行,不用有人手写fixture。

前端能从第一天就开工,靠的就是这一点。mock不是另一份需要有人维护的产物:它和客户端从同一份规范重新生成,所以不可能落后于规范。

Orval vs openapi-generator、openapi-typescript和Hey API
#

它们都从OpenAPI规范出发,只是走到的地方不一样。

生成什么最适合
Orval带类型的客户端函数或框架hooks(React Query、SWR、Vue、Svelte、Angular)、模型、MSW mock、Zod schema想用一份配置同时拿到hooks和mock的TypeScript前端
openapi-generator几十种语言的客户端和服务端桩代码,包括TypeScript需要把同一份规范转成Java、Kotlin、Swift和TypeScript的团队
openapi-typescript只生成类型,配合openapi-fetch得到一个轻量的带类型fetch封装只要类型,几乎不想要运行时代码
Hey API(@hey-api/openapi-ts)带类型的SDK,有TanStack Query、Zod等插件和Orval定位相近,围绕插件系统构建

如果只需要类型,openapi-typescript最轻,很难挑出毛病。openapi-generator的用武之地,是规范要喂给不止一种语言的时候;它的TypeScript输出能用,但读起来像是先为Java写的。Orval和Hey API之间,基本就看口味,以及你更愿意读哪一家生成的代码。我一直用Orval,是因为mock和hooks出自同一次运行,而正是这一点改变了两个团队的协作方式。

这不是什么光鲜的工具。它只是安安静静地消灭了团队之间一整类争吵,这笔买卖我每次都愿意做。

OpenApi - 这篇文章属于一个选集。
§ 2: 本文