API 类型生成与契约:tRPC、OpenAPI Codegen 与 GraphQL Codegen

系统覆盖 TypeScript 端到端 API 类型安全:tRPC 类型化过程、OpenAPI 规范与代码生成(openapi-typescript/swagger-typescript-api)、GraphQL Codegen、Schema 优先 vs Code 优先、前后端共享类型、运行时验证与类型生成对比,以及 API 类型生成的选型矩阵。

引言

前后端最痛的问题是「契约漂移」:后端改了响应字段,前端还按旧类型解析,直到运行时报错才发现。解决办法有三条主流路线——tRPC(前后端同进程/同库共享类型)、OpenAPI Codegen(规范驱动生成类型)、GraphQL Codegen(由 GraphQL Schema 生成类型)。三条路线都能让「改后端字段 → 前端编译报错」,但适用场景不同。

本文系统讲 API 类型生成:先对比「Schema 优先 vs Code 优先」,再深入 tRPC、OpenAPI Codegen、GraphQL Codegen 三套工具链,讲清各自的工作流、类型产出与取舍;接着覆盖前后端共享类型的最佳实践与运行时验证对比,最后给出选型矩阵。

前置:/typescript/(TS 基础)、/typescript-nodejs-backend/(Node 后端)、/typescript-runtime-validation-typesafe/(运行时验证)、/typescript-react-fullstack-typesafe/(全栈类型)。


目录


1. Schema 优先 vs Code 优先

1.1 两条路线的本质差异

维度Schema 优先Code 优先
单一来源契约文件(OpenAPI/GraphQL Schema)服务端代码
前端类型从契约生成从服务端类型导入/生成
前后端耦合低(契约解耦)高(同进程/同库)
跨语言强(任意语言生成)弱(TS 生态内)
变更控制契约评审先行代码即契约

1.2 三种路线概览

tRPC          :Code 优先,前后端 TS 同库 → 类型零成本共享
OpenAPI Codegen:Schema 优先,规范文件 → 生成 TS 类型/客户端
GraphQL Codegen :Schema 优先,.graphql 文件 → 生成 TS 类型

1.3 关键判断

前后端都是 TS 且耦合紧密 → tRPC(零成本、类型实时)
多语言/对外 API/需要文档 → OpenAPI(规范即文档)
查询灵活性高/聚合多源   → GraphQL

一句话总结:Code 优先(tRPC)胜在「零成本类型共享」,Schema 优先(OpenAPI/GraphQL)胜在「契约解耦与多语言」——先问前后端是否都 TS、是否需要对外文档。


2. tRPC:同库共享类型的端到端安全

2.1 tRPC 的核心

前后端共享「过程(procedure)定义」——类型自动推断,无 codegen
后端定义 router → 前端 import 类型 → 调用自动带类型

2.2 后端定义

// server/router.ts
import { initTRPC } from '@trpc/server'
import { z } from 'zod'

const t = initTRPC.create()

const appRouter = t.router({
  getUser: t.procedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input }) => {
      // 返回类型由实现推断
      return { id: input.id, name: 'Alice', role: 'user' }
    }),
  updateUser: t.procedure
    .input(z.object({ id: z.string(), name: z.string() }))
    .mutation(async ({ input }) => {
      // ... 更新
      return { success: true }
    }),
})

export type AppRouter = typeof appRouter

2.3 前端调用(类型自动推断)

// client/App.tsx
import { createTRPCProxyClient } from '@trpc/client'
import type { AppRouter } from '../server/router'   // 只 import 类型

const trpc = createTRPCProxyClient<AppRouter>({ links: [...] })

const user = await trpc.getUser.query({ id: '1' })   // user: { id, name, role }
user.role.toUpperCase()   // ✅ 类型正确
// trpc.getUser.query({ id: 1 })  ❌ input 类型错误(id 应为 string)

2.4 tRPC 的取舍

优点:零 codegen、类型实时同步、input 有 zod 运行时验证
代价:前后端必须 TS 同库(或共享类型包)、对「对外 REST API」不友好

一句话总结:tRPC 用「同库过程定义」实现端到端类型安全——后端写 router、前端 import 类型即可调用,input 配 zod 有运行时验证。


3. OpenAPI 规范与类型生成

3.1 OpenAPI 是什么

OpenAPI(Swagger)规范:用 YAML/JSON 描述 HTTP API
  → 路径、方法、请求/响应 Schema
  → 一份规范 = API 文档 + 代码生成器输入
# openapi.yaml(片段)
openapi: 3.0.0
paths:
  /users/{id}:
    get:
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
components:
  schemas:
    User:
      type: object
      required: [id, name]
      properties:
        id: { type: string }
        name: { type: string }

3.2 生成工作流

# 方式一:openapi-typescript(生成纯类型)
npx openapi-typescript openapi.yaml -o api-schema.ts

# 方式二:swagger-typescript-api(生成客户端 + 类型)
npx swagger-typescript-api -p openapi.yaml -o src/api -n client.ts

3.3 生成的类型长什么样

// api-schema.ts(openapi-typescript 生成)
export interface paths {
  '/users/{id}': {
    get: {
      parameters: { path: { id: string } }
      responses: { 200: { content: { 'application/json': components['schemas']['User'] } } }
    }
  }
}
export interface components {
  schemas: {
    User: { id: string; name: string }
  }
}

一句话总结:OpenAPI 规范即契约,openapi-typescript 生成纯类型、swagger-typescript-api 生成客户端+类型——规范一份,文档与前端类型同源。


4. openapi-typescript:从 OpenAPI 生成 TS 类型

4.1 集成到前端

import type { paths, components } from './api-schema'

type User = components['schemas']['User']   // { id: string; name: string }

// 类型化 fetch
async function getUser(id: string): Promise<User> {
  const res = await fetch(`/api/users/${id}`)
  if (!res.ok) throw new Error('bad')
  return res.json() as User
}

4.2 在 CI 中强制同步

# 后端变更 OpenAPI → 重新生成前端类型 → 类型检查
steps:
  - run: npx openapi-typescript ../../api/openapi.yaml -o src/api/schema.ts
  - run: pnpm typecheck   # 类型不匹配 → CI 失败

4.3 生成类型的注意

1. 响应类型只是「声明」,运行时仍要校验(加 zod)
2. OpenAPI 的 $ref/oneOf 会映射成复杂 TS 联合
3. 生成文件要提交进仓库(避免每次构建都生成)
4. 严格模式:nullable/optional 语义要对齐 tsconfig

一句话总结:openapi-typescript 把 OpenAPI 转成 TS 类型,CI 中「后端改规范 → 前端重新生成 → typecheck」让契约同步;运行时仍需 zod 兜底。


5. GraphQL Codegen:从 Schema 生成类型

5.1 GraphQL 的类型系统

# schema.graphql
type User { id: ID! name: String! posts: [Post!]! }
type Query { user(id: ID!): User }

5.2 GraphQL Codegen 生成

# codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli'
const config: CodegenConfig = {
  schema: './schema.graphql',
  documents: './src/**/*.graphql',
  generates: {
    './src/gql/': { preset: 'client' }   // 生成 types + hooks
  }
}
export default config

5.3 使用生成类型

// src/UserProfile.tsx
import { useUserQuery } from './gql/graphql'

function UserProfile({ id }: { id: string }) {
  const { data } = useUserQuery({ variables: { id } })
  // data.user.name 类型精确;data.user.xxx 不存在则报错
  return <div>{data?.user?.name}</div>
}

5.4 GraphQL Codegen 的取舍

优点:精确到「每个查询所需字段」的类型(字段级类型安全)
代价:需要 GraphQL 后端;文档与 codegen 配置有学习成本

一句话总结:GraphQL Codegen 从 Schema + 查询文档生成「字段级精确」的 types 与 hooks——前端只拿到查询过的字段类型,杜绝「多取/少取」漂移。


6. 前后端共享类型最佳实践

6.1 Monorepo 共享类型包

方案一(tRPC):前后端同库 import router 类型 → 零共享成本
方案二(类型包):@repo/types 放 DTO → 两端 import
方案三(生成):契约文件 → 两端各自生成(OpenAPI/GraphQL)

6.2 生成 vs 共享的混合

推荐混合:
  对外 REST API → OpenAPI Codegen(契约解耦 + 文档)
  内部全栈功能 → tRPC(零成本实时)
  复杂查询     → GraphQL Codegen
按「耦合度 + 对外需求」分段选型,不强制统一

6.3 关键实践

1. 生成文件提交入库,CI 中「重新生成 + typecheck」防漂移
2. 运行时验证(zod)与生成类型并存:生成声明、zod 校验
3. 契约变更走 PR 评审,前端类型不匹配即阻断
4. 版本策略:破坏性变更进大版本

一句话总结:共享类型最佳实践 = 「按耦合度混合选型 + CI 强制同步 + 生成与运行时验证并存」——不让契约漂移,也不让单一方案绑架全栈。


7. 运行时验证 vs 类型生成

维度类型生成运行时验证(zod)
作用时机编译期运行期
解决的问题前后端类型不一致数据实际不符合声明
实现生成 .d.ts/客户端Schema.parse 校验
关系声明「应该是这样」验证「实际是这样」
二者互补而非替代:
  类型生成:让「类型声明的修改」在编译期被发现
  zod     :让「运行时拿到的数据」被验证,非法即抛错
最佳实践:生成类型 + zod Schema 各管一层

一句话总结:类型生成管「编译期契约」,zod 管「运行时事实」——生成声明 + 运行时校验并存,是 API 契约防漂移的双保险。


8. 版本管理与契约演进

8.1 向后兼容的演进

加字段:向后兼容(旧前端忽略新字段)
改字段类型/删除字段:破坏性变更
重命名:破坏性变更

演进规则:
  1. 加字段优先(兼容)
  2. 破坏性变更用「新增端点/版本」
  3. 字段废弃用 deprecated 标记(OpenAPI/GraphQL 都支持)

8.2 API 版本化

URL 版本:/api/v1/users、/api/v2/users(常见)
Header 版本:Accept: application/vnd.app.v2+json
GraphQL:无 URL 版本,靠 Schema 演进 + 字段 deprecated

前端策略:按版本生成类型 → 逐步迁移,老版本类型保留

一句话总结:契约演进遵循「加字段兼容、破坏性变更进新版本」——OpenAPI 支持 URL/Header 版本化,GraphQL 用 deprecated 标记渐进演进。


9. 选型矩阵

场景推荐理由
前后端全 TS、内部应用tRPC零成本实时类型
对外 REST API、需文档OpenAPI Codegen契约即文档
复杂查询、多源聚合GraphQL Codegen字段级精确
多语言客户端OpenAPI Codegen任意语言生成
快速迭代 MVPtRPC少配置
微服务契约治理OpenAPI规范评审

混合架构示例:

对外网关 REST → OpenAPI 规范 → 生成 SDK 给外部/多语言
内部服务间     → tRPC(同 TS)→ 类型实时
前端复杂查询   → GraphQL → Codegen hooks
共享 DTO 放 @repo/types → 三处统一引用

一句话总结:选型按「耦合度与对外需求」——内部 tRPC、对外 OpenAPI、复杂查询 GraphQL;混合架构用共享类型包统一 DTO。


10. 速查表

需求方案
前后端类型实时同步tRPC
对外 API 文档 + 类型OpenAPI Codegen
字段级精确类型GraphQL Codegen
生成纯类型openapi-typescript
生成客户端+类型swagger-typescript-api
运行时校验zod(与生成并存)
契约防漂移CI 重新生成 + typecheck
破坏性变更新端点/版本
共享 DTO@repo/types 类型包
多语言OpenAPI 生成 SDK

一句话记忆:API 类型生成三路线——tRPC(Code 优先,同库类型实时)、OpenAPI(Schema 优先,规范即文档)、GraphQL(Schema 优先,字段级精确);选型按「内部 tRPC / 对外 OpenAPI / 复杂查询 GraphQL」;生成管编译期契约、zod 管运行时事实,双保险防漂移;CI 强制「重新生成 + typecheck」,破坏性变更进版本——让「改后端字段,前端编译报错」成为默认纪律。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「typescript」更多文章

  1. TypeScript 错误处理:Result 模式、类型化错误与错误边界实战
  2. TypeScript 测试策略:单元测试、类型测试与测试替身实战
  3. TypeScript 构建性能优化:增量编译、缓存与工具链选型