API 层设计
API 层不是“把 fetch 包一下”
这是很多团队最容易低估的地方。
表面看,API 层像是一些请求函数:
getUsercreateOrdersearchProducts
但真正进入工程规模之后,你会发现它承载的远不止这些。
它同时在处理:
- 协议和路径
- 鉴权
- 错误格式
- 类型约束
- 缓存与重试
- 请求去重
- 超时
- 日志和可观测性
- 前后端边界
所以 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.getProfileorderApi.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,不改错误分类