API 层设计

API 层设计——怎样把协议、契约、类型、错误、缓存、重试和鉴权放进一条清晰边界里,让组件消费数据时更稳定,也让前后端协作更可预测。

API 层设计

API 层不是“把 fetch 包一下”

这是很多团队最容易低估的地方。

表面看,API 层像是一些请求函数:

  • getUser
  • createOrder
  • searchProducts

但真正进入工程规模之后,你会发现它承载的远不止这些。

它同时在处理:

  • 协议和路径
  • 鉴权
  • 错误格式
  • 类型约束
  • 缓存与重试
  • 请求去重
  • 超时
  • 日志和可观测性
  • 前后端边界

所以 API 层的本质不是“请求代码放哪里”。

而是“前端和服务端如何建立一个稳定、可演进、可消费的契约层”。

先把“API 层”这个词讲清楚

对前端来说,API 层可以简单理解为:

业务组件和远端数据之间那一层统一边界。

它不应该让页面直接关心太多底层细节,例如:

  • token 怎么带
  • 错误码怎么解
  • 数据怎么转类型
  • 请求失败后怎么重试

如果这些细节都散在业务组件里,系统一大,维护成本会迅速上升。

为什么很多项目会在 API 层上越写越乱

常见原因有几个:

  • 一开始项目小,直接写 fetch 很快
  • 后面需求变多,又不断加拦截、重试、错误码分支
  • 不同人按自己的习惯封装
  • 服务端返回格式不统一
  • query 层、SDK 层、领域层没有清楚分工

最后就会出现一种很典型的状态:

所有人都知道“应该统一”,但实际没有人能说清到底统一什么。

一个成熟 API 层首先要统一什么

不是统一成某个库。

而是至少统一这几件事:

1. 契约来源

接口形状到底以什么为准。

例如:

  • OpenAPI
  • GraphQL schema
  • tRPC router
  • 手写 SDK 契约

2. 错误语义

什么叫网络错误,什么叫业务错误,什么叫权限错误,什么叫用户可恢复错误。

3. 类型流转

从接口定义到前端消费,中间类型是否一致。

4. 生命周期管理

请求是一次性拿完就算,还是要缓存、刷新、去重、重试、失效。

5. 使用边界

业务组件能不能直接发请求。

如果不能,应该通过什么层来拿数据。

REST、GraphQL、tRPC 到底怎么判断

不要把它们理解成“只是在请求写法上不同”。

它们背后的组织方式差别很大。

REST

REST 的优势通常在于:

  • 团队熟悉
  • HTTP 语义自然
  • 缓存和代理生态成熟
  • 和 OpenAPI 体系结合方便

它的问题通常不是 REST 本身。

而是很多团队会把 REST 接口设计得很随意,最后只剩 URL 风格是 REST。

GraphQL

GraphQL 的核心价值不是“请求可以随便拿字段”这么简单。

它真正解决的是:

  • 复杂数据聚合
  • 多端共享 schema
  • 前端按需声明数据依赖

但它也会带来额外复杂度:

  • schema 设计
  • 缓存策略
  • N+1 和服务端聚合成本
  • 客户端状态与 query 结果的边界

tRPC

tRPC 的价值在于端到端 TypeScript 推断。

在全栈 TS、monorepo、团队边界相对紧密的环境里,它的开发体验会非常顺。

但它的前提也很明确:

  • 你接受 TypeScript 作为共享契约基础
  • 前后端工程边界允许更紧耦合

所以它不是“通用标准答案”。

OpenAPI 在现代 API 层里为什么仍然很重要

因为它把契约从“口头共识”变成了结构化描述。

这带来几个直接收益:

  • 可以生成类型
  • 可以生成客户端
  • 可以做 mock
  • 可以做契约测试
  • 可以在跨团队场景里减少误解

openapi-typescript 这类工具的价值就很直接:

把 schema 变成运行时零负担的 TypeScript 类型。

这不是小优化。

它直接提高了协作和演进的稳定性。

API 层通常可以拆成哪几层

一个实用的分层思路通常是:

1. Transport 层

负责最底层的请求发出。

处理:

  • base URL
  • headers
  • timeout
  • token 注入
  • tracing
  • 基础日志

2. Endpoint / Client 层

把具体端点组织成清晰函数。

例如:

  • userApi.getProfile
  • orderApi.create

这一层应该尽量把路径和请求细节藏起来。

3. Domain / Adapter 层

有些接口返回并不适合直接给页面。

这时可以在这里做:

  • 字段整理
  • 领域对象映射
  • 错误语义转换
  • 兼容新旧接口

4. Query / UI Consumption 层

例如 TanStack Query hook。

这一层更关注:

  • 缓存
  • loading
  • error
  • 重试
  • 失效
  • 乐观更新

为什么不建议组件里到处直连请求

不是说绝对不能写。

而是当一个项目规模上来后,组件直连请求会带来几类问题:

  • token 处理散落
  • 错误处理散落
  • 请求 key 不统一
  • 类型和数据转换散落
  • 测试更难做

这会让“看似快”的写法在后期变得非常慢。

错误设计为什么是 API 层成熟度的分水岭

很多团队 API 层最大的问题,不是不会请求。

而是错误语义混乱。

常见混乱包括:

  • HTTP 失败和业务失败混在一起
  • 后端返回错误格式不统一
  • 前端只能拿 message 字符串硬判断
  • 某些错误该 toast,某些该静默,没人说得清

成熟 API 层通常会明确分层:

网络错误

例如断网、DNS、超时、跨域等。

协议错误

例如 401、403、404、500。

业务错误

例如库存不足、表单校验失败、状态不允许变更。

可恢复与不可恢复错误

用户能不能通过重试、重新登录、修改输入来自救。

为什么“统一错误格式”很值钱

因为它能把 UI 层从一堆分散判断里解放出来。

例如你可以更自然地决定:

  • 是否展示 toast
  • 是否跳登录
  • 是否记录异常
  • 是否允许局部重试

这也是为什么很多 API 层需要一个明确的 error normalization。

类型安全到底应该做到哪一层

一个成熟的 API 层,不应该只是在组件使用时才想起类型。

更好的目标通常是:

  • 契约有类型来源
  • 请求参数有类型
  • 响应数据有类型
  • 运行时关键边界有校验
  • 组件拿到的是更稳定的消费形态

这里要特别提醒一点:

TypeScript 不是运行时校验。

如果接口来源不可信,仍然需要在边界上做 runtime validation。

为什么 query 层和 API 层不能混成一层

因为它们关注的问题不同。

API 层更像“怎么拿到数据、怎么解释协议”。

query 层更像“怎么在界面里管理这份远端数据的生命周期”。

如果把两者揉成一个超大封装,最后往往很难维护。

更清晰的做法通常是:

  • API client 负责请求与返回
  • query hook 负责缓存和 UI 状态

缓存与失效为什么已经是 API 设计的一部分

现代前端,尤其在 React / Next.js 语境里,缓存不再只是“性能优化”。

它直接影响:

  • 用户看到的数据是否新鲜
  • 提交后界面何时更新
  • 列表与详情是否一致
  • 刷新和回退体验

所以 API 层设计一定要和缓存策略联动。

这也是为什么只讨论“怎么写请求函数”远远不够。

API 版本与兼容性应该怎么想

很多团队直到接口开始频繁破坏兼容时,才意识到 API 设计是长期资产。

成熟一点的做法通常会提前考虑:

  • 字段新增如何兼容
  • 字段废弃如何迁移
  • 是否需要版本化
  • 前端如何同时适配新旧接口

这些事情如果不在 API 层集中处理,很容易散落到各个页面。

Mock 和契约测试为什么值得进入 API 层

因为前后端协作里,最大成本之一是等待和误解。

如果契约清晰,你就可以更容易地做:

  • mock server
  • 类型生成
  • 契约测试
  • 联调前自测

这会让 API 层从“请求封装”升级成“协作接口”。

中国互联网语境里,API 层常见的复杂度来源

很多国内业务会同时遇到:

  • BFF 和直连接口并存
  • 新老接口长期共存
  • 登录态、风控、灰度、埋点要求很多
  • 多端复用但不完全同构

这会让 API 层更需要:

  • 清晰的错误语义
  • 兼容层
  • 领域适配层
  • 更强的联调与 mock 能力

海外产品语境里,哪些点更突出

在很多 SaaS、平台产品里,API 层常常更强调:

  • 公共契约稳定性
  • schema-first
  • SDK 化
  • typed client
  • observability

这和更强的多团队协作、开放平台和长期 API 生命周期管理有关。

这类主题为什么很适合做 senior 表达训练

因为它很容易被讲成工具对比:

  • REST vs GraphQL vs tRPC

这只是开头。

真正更像 senior 的表达应该继续说明:

  • API 层到底在统一什么
  • 为什么错误语义是关键
  • 为什么 query 层和 transport 层要分开
  • 为什么契约来源会影响团队协作方式

建议实践

实践 1:给现有项目画一张 API 分层图

练什么:

分清 transport、client、adapter、query 的职责。

最小交付物:

一张 API layer 结构图。

验收标准:

  • 每层职责清楚
  • 能指出当前耦合点

常见误区:

  • 只按文件夹命名,不按职责判断

实践 2:接 OpenAPI 自动生成类型

练什么:

把契约和类型流打通。

最小交付物:

一套由 schema 生成的 TypeScript 类型和最小 client。

验收标准:

  • 改 schema 后能影响调用侧类型
  • 运行时代码没有额外肥大

常见误区:

  • 生成完类型后,业务层仍然手写重复定义

实践 3:做一次统一错误归一化

练什么:

把分散错误处理收敛成稳定语义。

最小交付物:

一个 normalizeApiError 模块。

验收标准:

  • 至少区分网络、协议、业务三类错误
  • UI 层能基于归一化结果做稳定处理

常见误区:

  • 只改 message,不改错误分类

延展阅读