【前后端联调】接口代码生成 - hono + typescript + openapi 最佳实践

背景:在团队协作开发,前后端开发接口对齐永远是一道难题。在TS的世界里要保证类型安全往往浪费不必要的时间在定义类型上了。

实践方案:遵循 design-first,先设计接口,确定openapi文档。然后生成服务端API模板和前端请求SDK,保证类型安全,同时节省繁琐重复的代码编写时间。

约定 openAPI文档(通过apifox等方式)

这里展示使用apifox导出 openAPI描述文件。

一份简单的openAPI文档的json格式描述如下(默认模版.openapi.json):

json 复制代码
{
  "openapi": "3.0.1",
  "info": {
    "title": "默认模块",
    "description": "",
    "version": "1.0.0"
  },
  "tags": [],
  "paths": {
	"/sessions": {
      "post": {
        "summary": "登录",
        "deprecated": false,
        "description": "",
        "tags": [],
        "parameters": [],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string"
                  },
                  "password": {
                    "type": "string"
                  }
                },
                "required": [
                  "email",
                  "password"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {}
                }
              }
            }
          }
        },
        "security": []
      }
    },
  },
  "components": {
    "schemas": {},
    "responses": {},
    "securitySchemes": {}
  },
  "servers": [],
  "security": []
}  
  

paths下定义了你的路由(包括路径、方法、入参、响应等等)。

openAPI一般有两个导出形式json和yaml。这里简单起见,只放了1个登录接口(/sessions)的定义。

后端

后端框架,有很多选项,比如express/koa,hono,nest等等。我选择了hono,主要因为能支持bun/node多运行时和性能不错。

生成 hono 代码有两种比较推荐的方式:

下面主要介绍使用 hono-takibi 。如果是需要生成其他TS服务端框架的模板代码,可以选择使用Kubb 。如果是针对Java Springboot 则使用openapi-generator

hono-takibi生成模板

首先介绍下@hono/zod-openapi,这个是在hono框架的基础上,提供了http入参校验(基于zod)和文档生成(代码即文档)。

使用hono-takibi生成的代码是基于@hono/zod-openapi。基于json/yaml文件生成命令如下:

bash 复制代码
npx hono-takibi [path/openapi.json] -o [path/routes.ts]

比如我在项目根目录下,生成模板代码。

以登录注册为例,下面是生成的服务端代码routes.ts

ts 复制代码
import { createRoute, z } from '@hono/zod-openapi'
export const postSessionsRoute = createRoute({
  method: 'post',
  path: '/sessions',
  tags: [],
  summary: '登录',
  request: {
    body: {
      content: {
        'application/json': {
          schema: z
            .object({ email: z.string(), password: z.string() })
            .openapi({ required: ['email', 'password'] }),
        },
      },
    },
  },
  responses: { 200: { content: { 'application/json': { schema: z.object({}) } } } },
  security: [],
})


export const postUsersRoute = createRoute({
  method: 'post',
  path: '/users',
  tags: [],
  summary: '注册',
  request: {
    body: {
      content: {
        'application/json': {
          schema: z
            .object({
              email: z.string().openapi({ title: '邮箱' }),
              password: z.string().openapi({ title: '密码' }),
              emailCode: z.string().openapi({ title: '邮箱验证码' }),
              inviteCode: z.string().exactOptional().openapi({ title: '邀请码' }),
            })
            .openapi({ required: ['email', 'password', 'emailCode'] }),
        },
      },
    },
  },
  responses: { 200: { content: { 'application/json': { schema: z.object({}) } } } },
  security: [],
})

存在这么几个问题:

  1. 对于请求体的内容request.body.content 可以发现schema 是内联的,这不利于复用zod schema(不利于类型复用)。同理 还有request.queryresponses等。
  2. responses这里的响应体结构,没有做到复用。你可以看到{ 200: { content: {...}}}这种重复,这种不利于统一维护。

那么如何解决?

  1. 针对内联schema,我们这样解决:在apifox设计阶段,建立数据模型(这个是对应到服务端的DTO对象),最好是符合命名规范,对于query/body中的入参命名为XxxDto,对于响应结果命名为XxxResponseDto
  2. 针对响应体结构,定义统一的响应组件:在apifox设计阶段,建立响应组件(针对不同状态码200/201/400/401等)。同时针对固定响应结构需要设计一个数据模型ApiResponseDto 来填充。

数据模型和一些特定状态码的响应结构:

登录接口示例:

最终 再使用 hono-takibi 生成一下服务端 代码,如下:

ts 复制代码
import { createRoute, z } from '@hono/zod-openapi'

const UserResponseDtoSchema = z
  .object({ id: z.string(), email: z.string(), username: z.string(), avatar: z.string() })
  .openapi({ required: ['id', 'email', 'username', 'avatar'] })
  .openapi('UserResponseDto')

const CreateUserDtoSchema = z
  .object({
    email: z.string().openapi({ title: '邮箱' }),
    password: z.string().openapi({ title: '密码' }),
    emailCode: z.string().openapi({ title: '邮箱验证码' }),
    inviteCode: z.string().exactOptional().openapi({ title: '邀请码' }),
  })
  .openapi({ required: ['email', 'password', 'emailCode'] })
  .openapi('CreateUserDto')

const LoginDtoSchema = z
  .object({ email: z.string(), password: z.string() })
  .openapi({ required: ['email', 'password'] })
  .openapi('LoginDto')

const ApiResponseDtoSchema = z
  .object({
    code: z.int().openapi({ description: '业务号码' }),
    data: z.object({}).nullable().openapi({ description: '业务数据' }),
    message: z.string().exactOptional().openapi({ description: '消息' }),
  })
  .openapi({ required: ['code', 'data'] })
  .openapi('ApiResponseDto')

const LoginResponseDtoSchema = z
  .object({
    accessToken: z.string().openapi({ description: '身份token' }),
    user: UserResponseDtoSchema,
  })
  .openapi({ required: ['accessToken', 'user'] })
  .openapi('LoginResponseDto')

const SuccessNullResponse = {
  description: '无内容的成功响应',
  content: { 'application/json': { schema: ApiResponseDtoSchema } },
}

const UnprocessableResponse = {
  description: '无法处理请求,失败响应',
  content: { 'application/json': { schema: ApiResponseDtoSchema } },
}

export const postSessionsRoute = createRoute({
  method: 'post',
  path: '/sessions',
  tags: [],
  summary: '登录',
  request: { body: { content: { 'application/json': { schema: LoginDtoSchema, examples: {} } } } },
  responses: {
    200: {
      description: '成功',
      headers: z.object({}),
      content: {
        'application/json': {
          schema: z
            .object({
              code: z.int().openapi({ description: '业务号码' }),
              data: LoginResponseDtoSchema.nullable().openapi({ description: '业务数据' }),
              message: z.string().exactOptional().openapi({ description: '消息' }),
            })
            .openapi({ required: ['code', 'data'] }),
        },
      },
    },
    400: UnprocessableResponse,
  },
  security: [],
})

export const postUsersRoute = createRoute({
  method: 'post',
  path: '/users',
  tags: [],
  summary: '注册',
  request: {
    body: { content: { 'application/json': { schema: CreateUserDtoSchema, examples: {} } } },
  },
  responses: {
    200: {
      description: '成功',
      headers: z.object({}),
      content: {
        'application/json': {
          schema: z
            .object({
              code: z.int().openapi({ description: '业务号码' }),
              data: z.object({}).nullable().openapi({ description: '业务数据' }),
              message: z.string().exactOptional().openapi({ description: '消息' }),
            })
            .openapi({ required: ['code', 'data'] }),
        },
      },
    },
    400: UnprocessableResponse,
  },
  security: [],
})

P.S.有个小问题,就是你定义 响应组件/响应时,一定要定义描述(description),否则生成代码会出现TS问题。

定义了description的效果:

集成到hono提供API服务

需要注意:前面提到的routes.ts中生成的xxxRoute是对参数校验和文档描述。实际上的路由逻辑是下面这样的:

ts 复制代码
import { OpenAPIHono } from '@hono/zod-openapi'
import { cors } from 'hono/cors'
import { postSessionsRoute, postUsersRoute } from './routes'

const app = new OpenAPIHono()

app.use(
  '/*',
  cors({
    origin: '*',
  })
)

app.get('/', (c) => {
  return c.text('Hello Hono!')
})

app.openapi(postSessionsRoute, async (c) => {
  const { password } = c.req.valid('json')
  if (password.length < 6) {
    return c.json(
      {
        code: 400,
        data: null,
        message: '密码长度小于6',
      },
      400
    )
  }
  // TODO: 实现登录逻辑
  return c.json({
    code: 0,
    data: {
      accessToken: 'token',
      user: {
        id: '1',
        email: 'user@example.com',
        username: 'user',
        avatar: '',
      },
    },
  })
})

app.openapi(postUsersRoute, async (c) => {
  const { password } = c.req.valid('json')
  if (password.length < 6) {
    return c.json(
      {
        code: 400,
        data: null,
        message: '密码长度小于6',
      },
      400
    )
  }
  // TODO: 实现注册逻辑
  return c.json({
    code: 0,
    data: null,
  })
})

export default app

使用 bun dev 运行(这是一个honojs项目),然后在apifox中测试如下:

前端

生成TS客户端代码,选择就很多了

  • 最轻量:openapi-typescript + openapi-fetch。见openapi-ts.dev
  • 框架党首选:Orval (配合 TanStack Query/React Query),见orval.dev
  • 体系一致性方案:Kubb,前后端都采用Kubb。见kubb.dev
  • @hey-api/openapi-ts FastAPI官方就推荐 这个。见heyapi.dev

这里没有推荐 openapi-generator 了,因为确实存在一些局限性,生成的前端SDK并不好用,在TS安全类型上不如其他选择(运行这个工具还要折腾Java环境)。

下面重点介绍下 openapi-typescript + openapi-fetchhey-api 2种方式。

openapi-typescript + openapi-fetch

使用

1.安装 两个依赖

bash 复制代码
pnpm add openapi-typescript -D
pnpm add openapi-fetch

2.运行openapi-typescript 生成ts类型

bash 复制代码
npx openapi-typescript "../默认模块.openapi.json" -o "app/utils/openapi/schema.d.ts"

3.编写客户端代码

ts 复制代码
import createClient from "openapi-fetch";
import type { paths } from "./schema";

export const client = createClient<paths>({ baseUrl: "http://127.0.0.1:3000" });

// Export type for use in components
export type Client = typeof client;

4.一个登录例子:

ts 复制代码
const { 
data,  // only present if 2XX response
error  // only present if 4XX or 5XX response
} = await client.POST("/sessions", {
    body: {
        email,
        password,
    },
});

if (error) {
	setStatus("error");
	setMessage(error.message || "登录失败");
} else if (data) {
	setStatus("success");
	setMessage("登录成功!");
	console.log("Login successful:", data);
	// 这里可以处理登录成功后的逻辑,比如保存 token 或跳转
	// const token = data.data?.accessToken;
}

一个好的 fetch 包装器绝对不应该使用泛型。 泛型需要更多的输入,而且可能会隐藏错误!

可以看出client 提供了GETPOST等方法,熟悉的写法,传入url和body参数。返回的结果包括data和error,data就是我们前面定义的ApiResponseDto,如下:

ts 复制代码
const data: {
    code: number;
    data: {
        accessToken: string;
        user: {
            id: string;
            email: string;
            username: string;
            avatar: string;
        };
    };
    message?: string | undefined;
} | undefined

而 error,就是当状态码不是2xx时,不空,类型是UnprocessableResponse | XxxErrorResponse类型,如下:

ts 复制代码
const error: {
    code: number;
    data: Record<string, never> | null;
    message?: string;
} | undefined
特性

1.支持的请求库如下:

Library Size (min)
openapi-fetch 6 kB
openapi-typescript-fetch 3 kB
feature-fetch 15 kB
openapi-axios 32 kB

2.支持「中间件」。

使用axios的同学,肯定对请求拦截和响应拦截不陌生,而openapi-fetch 提供中间件完成同样的功能:

ts 复制代码
import createClient, { type Middleware } from "openapi-fetch";
import type { paths } from "./schema";



const myMiddleware: Middleware = {
  async onRequest({ request, options }) {
    // set "Authorization" header,认证
    request.headers.set("Authorization", "Bearer " + "your_access_token"); 
    return request;
  },
  async onResponse({ request, response, options }) {
    const { body, ...resOptions } = response;
    console.log('body', body); // ReadableStream
    console.log('response', response);
    if (response.status === 401) {
      const error = new Error("Unauthorized");
      (error as Error & { status?: number }).status = 401;
      
      window.location.href = "/login";
      return
    }

    return response;
    // 或者 return new Response(body, { ...resOptions});
  },
  async onError({ error }) {
    // wrap errors thrown by fetch
    console.log('error', error);
    if (error instanceof Error) {
      return error;
    }
    return new Error("Oops, fetch failed", { cause: error });
  },
};


export const client = createClient<paths>({ baseUrl: "http://127.0.0.1:3000" });


// register middleware
client.use(myMiddleware);

// Export type for use in components
export type Client = typeof client;

需要注意,openapi-fetch 一般不会抛出错误,比如401/403之类错误状态码(除非你在onResponse中手动抛出错误)。onError 回调函数允许你处理 fetch 抛出的错误。常见的错误包括 TypeError (当出现网络或 CORS 错误时可能发生)和 DOMException (当使用 AbortController 中止请求时可能发生)。

3.支持使用 DTO类型

之前生成的schema.d.ts中定义了interface components:

ts 复制代码
export interface components {
    schemas: {
        UserResponseDto: {
            id: string;
            email: string;
            username: string;
            avatar: string;
        };
        CreateUserDto: {
            /** 邮箱 */
            email: string;
            /** 密码 */
            password: string;
            /** 邮箱验证码 */
            emailCode: string;
            /** 邀请码 */
            inviteCode?: string;
        };
        LoginDto: {
            email: string;
            password: string;
        };
        ...

可以这样使用:

ts 复制代码
import { client , type components } from "~/utils/openapi";

const body: components["schemas"]["LoginDto"] = {
      email,
      password,
    }

4.对框架的支持

通过openapi-react-query库,也能支持结合tanstack query使用。use-query

hey-api

基础使用和特性

1.hey-api生成代码时,会创建一个文件夹(默认是"client")存放内容,和openapi-typescript相比,是它生成了一个默认的client,并且每个API都提供了方法直接调用(无需路径)

2.安装

bash 复制代码
pnpm add @hey-api/openapi-ts -D

3.配置openapi-ts命令

json 复制代码
"scripts": {
    "build": "react-router build",
    "dev": "react-router dev",
    "openapi-ts": "openapi-ts --input ../默认模块.openapi.json --output ./app/utils/heyapi"
  }

参数复杂了,也可以放配置文件/openapi-ts.config.ts中,比如:

js 复制代码
import { defineConfig } from '@hey-api/openapi-ts';

export default defineConfig({
  input: 'http://127.0.0.1:8800/openapi.json', //支持远程url和本地openapi文件
  output: './app/APIs',
  plugins: [{
      name: '@hey-api/client-fetch',
      runtimeConfigPath: '@/hey-api',  // 控制client.gen.ts生成 
    },
  ], 
});

4.执行pnpm openapi-ts. 生成的sdk代码都在heyapi目录下,比较复杂。请求方法的代码都生成在sdk.gen.ts,而DTO类型都生成在types.gen.ts中。

复制代码
app/
├── utils/
│ ├── heyapi/
│ │ ├── client/
│ │ ├── core/
│ │ ├── client.gen.ts
│ │ ├── index.ts
│ │ ├── sdk.gen.ts
│ │ └── types.gen.ts
│ └── index.ts

5.看看是如何使用的吧:

ts 复制代码
import { postSessions, type LoginDto } from "~/utils/heyapi";

import { client } from "~/utils/heyapi/client.gen";

// 需要先做一些基础的client配置 (也支持自己重新创建一个新 client)
client.setConfig({
  baseUrl: "http://127.0.0.1:3000",
});


const body: LoginDto = {
      email,
      password,
    }

const { data, error } = await postSessions({
	body: body,
  });

  if (error) {
	setStatus("error");
	setMessage(error.message || "登录失败");
  } else if (data) {
	setStatus("success");
	setMessage("登录成功!");
	console.log("Login successful:", data);
	// 这里可以处理登录成功后的逻辑,比如保存 token 或跳转
	// const token = data.data?.accessToken;
  }

其中请求结果:data 和 error ,默认情况下 和 openapi-fetch的处理是一致的(都是不抛出错误,而是将错误通过error暴露)。

6.网络请求库方面也适配了fetch和axios,也支持tanstack query。并且计划未来对TS服务端框架支持,但还有很多没完成的(处在soon状态)。

结合 tanstack query

参考:plugin tanstack-query. 准备配置文件/openapi-ts.config.ts

js 复制代码
import { defineConfig } from '@hey-api/openapi-ts';

export default defineConfig({
  input: '../默认模块.openapi.json',
  output: './app/utils/heyapi',
  plugins: ['@tanstack/react-query'], 
});

执行 pnpm openapi-ts,此时生成的目录下多出一个文件 ./@tanstack/react-query.gen.ts

ts 复制代码
// This file is auto-generated by @hey-api/openapi-ts

import type { UseMutationOptions } from '@tanstack/react-query';

import { type Options, postSessions, postUsers } from '../sdk.gen';
import type { PostSessionsData, PostSessionsError, PostSessionsResponse, PostUsersData, PostUsersError, PostUsersResponse } from '../types.gen';

/**
 * 登录
 */
export const postSessionsMutation = (options?: Partial<Options<PostSessionsData>>): UseMutationOptions<PostSessionsResponse, PostSessionsError, Options<PostSessionsData>> => {
    const mutationOptions: UseMutationOptions<PostSessionsResponse, PostSessionsError, Options<PostSessionsData>> = {
        mutationFn: async (fnOptions) => {
            const { data } = await postSessions({
                ...options,
                ...fnOptions,
                throwOnError: true
            });
            return data;
        }
    };
    return mutationOptions;
};

/**
 * 注册
 */
export const postUsersMutation = (options?: Partial<Options<PostUsersData>>): UseMutationOptions<PostUsersResponse, PostUsersError, Options<PostUsersData>> => {
    const mutationOptions: UseMutationOptions<PostUsersResponse, PostUsersError, Options<PostUsersData>> = {
        mutationFn: async (fnOptions) => {
            const { data } = await postUsers({
                ...options,
                ...fnOptions,
                throwOnError: true
            });
            return data;
        }
    };
    return mutationOptions;
};

整个登录页面的代码如下:

tsx 复制代码
import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import {postSessionsMutation }  from "~/utils/heyapi/@tanstack/react-query.gen"
import { postSessions, type LoginDto } from "~/utils/heyapi";
import { client } from "~/utils/heyapi/client.gen";
import type { Route } from "./+types/login3";

// Configure the client
client.setConfig({
  baseUrl: "http://127.0.0.1:3000",
});

export function meta({}: Route.MetaArgs) {
  return [
    { title: "登录" },
    { name: "description", content: "用户登录" },
  ];
}

export default function Login() {
  const [email, setEmail] = useState("abc@example.com");
  const [password, setPassword] = useState("123456");

  const { mutate, isPending, isSuccess, error } = useMutation({
    // mutationFn: async (body: LoginDto) => {
    //   const { data, error } = await postSessions({
    //     body: body,
    //   });
    //   if (error) {
    //     throw error;
    //   }
    //   return data;
    // },
    ...postSessionsMutation(),
    onSuccess: (data) => {
      console.log("Login successful:", data);
      // 这里可以处理登录成功后的逻辑,比如保存 token 或跳转
      // const token = data.data?.accessToken;
    },
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    mutate({
      body: {
        email,
        password,
      }
    });
  };

  const status = isPending ? "loading" : isSuccess ? "success" : error ? "error" : "idle";
  const message = isSuccess
    ? "登录成功!"
    : error
    ? (error as any).message || "登录失败"
    : "";

  return (
    <div className="flex min-h-screen items-center justify-center bg-gray-50 px-4 py-12 sm:px-6 lg:px-8">
      <div className="w-full max-w-md space-y-8 bg-white p-8 shadow rounded-lg">
        <div>
          <h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
            登录您的账户3
          </h2>
        </div>
        <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
          <div className="-space-y-px rounded-md shadow-sm">
            <div>
              <label htmlFor="email-address" className="sr-only">
                邮箱地址
              </label>
              <input
                id="email-address"
                name="email"
                type="email"
                autoComplete="email"
                required
                className="relative block w-full rounded-t-md border-0 py-1.5 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6 px-3"
                placeholder="邮箱地址"
                value={email}
                onChange={(e) => setEmail(e.target.value)}
              />
            </div>
            <div>
              <label htmlFor="password" className="sr-only">
                密码
              </label>
              <input
                id="password"
                name="password"
                type="password"
                autoComplete="current-password"
                required
                className="relative block w-full rounded-b-md border-0 py-1.5 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6 px-3"
                placeholder="密码"
                value={password}
                onChange={(e) => setPassword(e.target.value)}
              />
            </div>
          </div>

          {message && (
            <div
              className={`text-sm text-center ${
                status === "success" ? "text-green-600" : "text-red-600"
              }`}
            >
              {message}
            </div>
          )}

          <div>
            <button
              type="submit"
              disabled={status === "loading"}
              className="group relative flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:opacity-50"
            >
              {status === "loading" ? "登录中..." : "登录"}
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}

技术选型

技术方案比较

在选择 TypeScript OpenAPI 客户端生成方案时,核心的权衡点在于:"运行时开销 vs. 开发体验" 以及 "灵活性 vs. 自动化程度"

维度 openapi-typescript (+ fetch) Orval Kubb @hey-api/openapi-ts
定位 极简/轻量主义 前端框架深度集成 工业级流水线/全栈体系 官方首选/生产级SDK
生成内容 仅生成 TS 类型定义 类型 + API 请求函数 + Hooks + Mocks 类型 + Hooks + 验证器 + 路由 + 模版 类型 + SDK + Hooks + 验证器
核心优势 零运行时开销。直接利用 TS 类型收窄,包体积增加几乎为 0。 TanStack Query 亲儿子。一键生成全套 React/Vue Query 钩子。 高度可插件化。支持用 JSX 写代码生成模版,前后端契约高度一致。 FastAPI 官方推荐。由原作者维护的升级版,支持 20+ 插件。
状态管理集成 无 (需手动配合 TanStack Query) 内置支持 TanStack Query, SWR 内置支持 TanStack Query 插件支持 TanStack Query, Pinia
校验支持 无 (仅编译期) 支持 Zod 支持 Zod, Faker 支持 Zod, Valibot
网络库 原生 fetch (通过 openapi-fetch) Axios, Fetch, Hook Axios, Fetch Fetch, Axios, Angular, Nuxt
Mock 支持 内置支持 MSW 内置支持 Faker 计划支持 (Chance)
适用场景 极度关注包体积、喜欢原生 API、对封装有"洁癖"的项目。 典型的中后台管理系统,深度使用 React/Vue Query 的项目。 复杂项目,需要自定义生成逻辑(如自动生成后端路由、Schema)的团队。 需要高度成熟稳定、符合 FastAPI 体系或大厂规范的 SDK。
学习曲线 极低 中 (需配置 orval.config.js) 高 (需理解插件系统/模版)

  1. 如果你追求 "极致轻量"
  • 选择: openapi-typescript + openapi-fetch
  • 理由: 它是目前最符合 TypeScript 原生思维的方案。它不生成成千上万行的 JS 代码,只生成类型。你的 API 调用看起来就像原生的 fetch,但带有完美的自动补全。
  1. 如果你是 "TanStack Query (React/Vue Query) 用户"
  • 选择: Orval
  • 理由: Orval 是目前生成 React Query Hooks 最成熟的工具。它能自动生成 queryKey、处理缓存逻辑、甚至自动生成 MSW 的 Mock 数据,极大提升开发效率。
  1. 如果你想要 "全栈体系一致性"
  • 选择: Kubb
  • 理由: Kubb 的野心更大,它不仅是为了前端。通过它的插件系统,你可以把一套 OpenAPI 定义同时转化为前端的 Hooks 和后端的路由定义(如 Hono/Elysia),确保前后端代码在结构上是"镜像"的。
  1. 如果你追求 "官方规范与工程化"
  • 选择: @hey-api/openapi-ts
  • 理由: 这是 openapi-typescript-codegen 的正统继任者。如果你的后端是 FastAPI,或者你希望生成的代码像一个正式的 SDK(有完整的类、方法封装),它是最稳妥的选择。它的插件系统(Plugin)也让它在功能上非常全能。

总结一句话:

  • 想简单:openapi-typescript
  • 想省事(前端):Orval
  • 想折腾/全栈:Kubb
  • 想标准/大而全:Hey API

我个人的话,就主要从hey-apiopenapi-typescript/openapi-fetch中选了:

  • 对于管理端、ToB的应用,使用hey-api 或者 Orval
  • 对于比较轻量化的h5页面使用openapi-typescript/openapi-fetch
相关推荐
mCell8 小时前
如何零成本搭建个人站点
前端·程序员·github
mCell9 小时前
为什么 Memo Code 先做 CLI:以及终端输入框到底有多难搞
前端·设计模式·agent
恋猫de小郭9 小时前
AI 在提高你工作效率的同时,也一直在增加你的疲惫和焦虑
前端·人工智能·ai编程
少云清9 小时前
【安全测试】2_客户端脚本安全测试 _XSS和CSRF
前端·xss·csrf
银烛木9 小时前
黑马程序员前端h5+css3
前端·css·css3
m0_607076609 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3
听海边涛声9 小时前
CSS3 图片模糊处理
前端·css·css3
IT、木易9 小时前
css3 backdrop-filter 在移动端 Safari 上导致渲染性能急剧下降的优化方案有哪些?
前端·css3·safari
0思必得010 小时前
[Web自动化] Selenium无头模式
前端·爬虫·selenium·自动化·web自动化
anOnion10 小时前
构建无障碍组件之Dialog Pattern
前端·html·交互设计