API First 实践:我们如何让前端类型可预测且稳定
Source: Dev.to
Source: …
问题:前端–后端不同步
在采用 API‑First 之前,我们的工作流大致如下:
- 后端 更改了一个端点。
- 前端 仍然使用旧的假设。
- 手动维护的 TypeScript 类型变得过时。
- 错误在后期才出现——有时甚至只在生产环境中出现。
即使沟通和文档做得再好,现实也很简单:
前端总是太晚才发现破坏性更改。
我们的解决方案:API‑First 作为契约,而非文档
在我们的团队中,API‑First 意味着一个简单的规则:
前端仅使用从后端 API 生成的 TypeScript 类型和接口。
OpenAPI(Swagger)模式 不仅仅是文档。它是 唯一的真实来源。如果某些内容未在契约中描述,前端不会假设它存在。
Source: …
实际运作方式(CRUD 用户示例)
1. 后端提供 OpenAPI 架构
在后端实现完成之前,后端团队会交付一个 YAML 格式的 OpenAPI 架构,描述未来的 API。下面是一个简化但真实的示例:
openapi: 3.0.3
info:
title: User Service API
description: API for managing users
version: "1.0"
paths:
/users:
get:
tags:
- users
summary: Get list of users
operationId: listUsers
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
- name: status
in: query
schema:
type: string
enum: [active, inactive]
responses:
"200":
description: List of users
content:
application/json:
schema:
$ref: "#/components/schemas/UserListResponse"
post:
tags:
- users
summary: Create new user
operationId: createUser
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateUserRequest"
responses:
"201":
description: User created
content:
application/json:
schema:
$ref: "#/components/schemas/User"
/users/{id}:
get:
tags:
- users
summary: Get user by ID
operationId: getUser
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
"200":
description: User data
content:
application/json:
schema:
$ref: "#/components/schemas/User"
components:
schemas:
User:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
email:
type: string
format: email
status:
type: string
enum: [active, inactive]
CreateUserRequest:
type: object
required:
- name
- email
properties:
name:
type: string
email:
type: string
format: email
UserListResponse:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/User"
total:
type: integer
这段 YAML 已足以让前端开始工作——即使后端仍在建设中。
2. 生成 TypeScript 类型和 API 请求函数
在前端我们使用 openapi-generator(或类似工具)根据该架构生成 TypeScript 代码。生成器会产出:
- TypeScript 类型和接口(
User、CreateUserRequest等) - 可直接使用的 API 请求函数(
listUsers、createUser、getUser)
我们不再手动编写 Axios 调用,也不需要猜测请求结构。所有导入的内容都直接来源于生成的代码,例如:
// Example usage
import { getUser, createUser, User, CreateUserRequest } from './api';
async function loadUser(id: string) {
const user: User = await getUser({ id });
// …
}
async function addUser(payload: CreateUserRequest) {
const newUser: User = await createUser(payload);
// …
}
Source: …
以生成的类型为前端代码的基础
一旦类型生成后,它们就成为前端整个系统的唯一真实来源:
- API 层 – 所有 HTTP 调用都使用生成的函数。
- React 组件 – props 和 state 使用生成的接口进行类型标注。
- 表单与校验 – 请求负载类型驱动表单 schema。
- UI 状态与选择器 – 枚举和字面量联合保持 UI 与后端同步。
示例:枚举生成
从 OpenAPI schema 中我们得到一个用于用户状态的生成枚举:
export const UserStatusEnum = {
Active: 'active',
Inactive: 'inactive',
} as const;
export type UserStatusEnum =
typeof UserStatusEnum[keyof typeof UserStatusEnum];
现在前端知道 User.status 只能是 "active" 或 "inactive"。我们使用这个枚举来:
- 构建下拉选项。
- 创建过滤列表。
- 驱动条件 UI 逻辑。
没有硬编码字符串,没有重复的常量,也没有静默的匹配错误。
CI 作为预警系统
当它成为 CI 流水线的一部分时,这种方法的真正威力便显现出来。
- 获取最新的 Swagger 架构(例如,从共享的制品库中)。
- 在每次 pull‑request 构建时重新生成 TypeScript 类型。
- 运行
tsc并执行完整的前端单元/集成测试套件。
如果后端出现了意外的更改:
- 生成的类型会变化。
- TypeScript 编译失败或测试出错。
由于失败发生在 代码合并之前,前端团队会立即收到合同已偏离的信号,从而能够即时协调修复。
TL;DR
- 将 OpenAPI 架构视为唯一可信来源。
- 从中生成 TypeScript 类型和请求函数。
- 让前端只使用这些生成的产物。
- 在 CI 中运行重新生成,以便及早捕获契约破裂。
通过强制执行这种先契约后实现的工作流,我们消除了经典的“前后端漂移”问题,减少了手动类型维护,并让团队对 UI 与 API 同步保持信心。
遇到的问题
- IPT 编译失败
- 单元测试失败
重要的是 何时 发生此情况:
- 不是在发布后
- 不是在生产环境中
- 在 CI 期间
这意味着后端和前端开发人员能够立即意识到问题。前端测试成为后端更改的额外安全网。
我们从这种方法中获得的收益
在采用 API‑First 之后,我们注意到明显的改进:
- 前端类型可预测且稳定
- 开发速度更快
- 团队之间的问题更少
- 更早发现破坏性更改
- 重构时信心大幅提升
前端不再 猜测,而是 信任合同。
如何在其他团队引入此做法
如果你想尝试这种方法,我的建议是:
- 先从一个服务开始
- 先生成类型,如有需要再生成请求
- 强制使用生成的类型
- 在 CI 中添加类型检查
- 将 OpenAPI 视为合同,而非文档
这更多是关于纪律,而不是工具。
最后思考
API‑First 并不是关于 Swagger 文件或生成器。
它关乎可预测性。
对于 TypeScript 前端来说,拥有可靠的契约会改变你对数据的思考方式。当 API 成为唯一可信来源时,前端会变得更简洁、更安全、更有信心。