引言
前后端最痛的问题是「契约漂移」:后端改了响应字段,前端还按旧类型解析,直到运行时报错才发现。解决办法有三条主流路线——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 优先
- 2. tRPC:同库共享类型的端到端安全
- 3. OpenAPI 规范与类型生成
- 4. openapi-typescript:从 OpenAPI 生成 TS 类型
- 5. GraphQL Codegen:从 Schema 生成类型
- 6. 前后端共享类型最佳实践
- 7. 运行时验证 vs 类型生成
- 8. 版本管理与契约演进
- 9. 选型矩阵
- 10. 速查表
- 延伸阅读
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 | 任意语言生成 |
| 快速迭代 MVP | tRPC | 少配置 |
| 微服务契约治理 | 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」,破坏性变更进版本——让「改后端字段,前端编译报错」成为默认纪律。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。