API First 实践:我们如何让前端类型可预测且稳定

发布: (2026年1月12日 GMT+8 05:09)
8 min read
原文: Dev.to

Source: Dev.to

Source:

问题:前端–后端不同步

在采用 API‑First 之前,我们的工作流大致如下:

  1. 后端 更改了一个端点。
  2. 前端 仍然使用旧的假设。
  3. 手动维护的 TypeScript 类型变得过时。
  4. 错误在后期才出现——有时甚至只在生产环境中出现。

即使沟通和文档做得再好,现实也很简单:

前端总是太晚才发现破坏性更改。

我们的解决方案: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 类型和接口(UserCreateUserRequest 等)
  • 可直接使用的 API 请求函数(listUserscreateUsergetUser

我们不再手动编写 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 流水线的一部分时,这种方法的真正威力便显现出来。

  1. 获取最新的 Swagger 架构(例如,从共享的制品库中)。
  2. 在每次 pull‑request 构建时重新生成 TypeScript 类型
  3. 运行 tsc 并执行完整的前端单元/集成测试套件。

如果后端出现了意外的更改:

  • 生成的类型会变化。
  • TypeScript 编译失败或测试出错。

由于失败发生在 代码合并之前,前端团队会立即收到合同已偏离的信号,从而能够即时协调修复。

TL;DR

  • 将 OpenAPI 架构视为唯一可信来源。
  • 从中生成 TypeScript 类型和请求函数。
  • 让前端只使用这些生成的产物。
  • 在 CI 中运行重新生成,以便及早捕获契约破裂。

通过强制执行这种先契约后实现的工作流,我们消除了经典的“前后端漂移”问题,减少了手动类型维护,并让团队对 UI 与 API 同步保持信心。

遇到的问题

  • IPT 编译失败
  • 单元测试失败

重要的是 何时 发生此情况:

  • 不是在发布后
  • 不是在生产环境中
  • 在 CI 期间

这意味着后端和前端开发人员能够立即意识到问题。前端测试成为后端更改的额外安全网。

我们从这种方法中获得的收益

在采用 API‑First 之后,我们注意到明显的改进:

  • 前端类型可预测且稳定
  • 开发速度更快
  • 团队之间的问题更少
  • 更早发现破坏性更改
  • 重构时信心大幅提升

前端不再 猜测,而是 信任合同

如何在其他团队引入此做法

如果你想尝试这种方法,我的建议是:

  1. 先从一个服务开始
  2. 先生成类型,如有需要再生成请求
  3. 强制使用生成的类型
  4. 在 CI 中添加类型检查
  5. 将 OpenAPI 视为合同,而非文档

这更多是关于纪律,而不是工具。

最后思考

API‑First 并不是关于 Swagger 文件或生成器。
它关乎可预测性。

对于 TypeScript 前端来说,拥有可靠的契约会改变你对数据的思考方式。当 API 成为唯一可信来源时,前端会变得更简洁、更安全、更有信心。

Back to Blog

相关文章

阅读更多 »

React 组件中的 TypeScript 泛型

介绍:泛型并不是在 React 组件中每天都会使用的东西,但在某些情况下,它们可以让你编写既灵活又类型安全的组件。