API设计血泪史:三次重写教会我的那些事

2026-07-10 1 0

做过独立开发的人都知道,最痛苦的不是从零开始写代码,而是接手一个别人写的、烂到无法维护的API项目。

我经历过三次API重写,不是为了炫技,是因为真的撑不下去了。今天不整那些"RESTful最佳实践"的虚的,就说说我踩过的坑,以及怎么避开它们。

教训一:URL里的版本号,毁掉了我第一个项目

当年年少轻狂,觉得RESTful嘛,/api/v1/users 很标准,升级就 /api/v2/users,多清晰。结果呢?

GET /api/v1/users/123
GET /api/v2/users/123

两个端点,两套逻辑,两个数据库查询。

三个月后,v1 还有人在用——因为某些老客户的App还没更新。你怎么办?继续维护两套代码?每次改一个字段要改两个地方?

后来我才明白:URL版本号是给人类看的,不是给机器看的。它最大的作用是让产品经理在发版说明里能写"全新升级的v2版本",显得很专业。至于技术实现?这只是把版本管理的复杂度从代码层搬到了URL层,并不会让问题消失。

真正的问题是:你什么时候该升级?升级的判断标准是什么?

我的经验是:只在有破坏性变更时才需要新版本。而破坏性变更应该尽量避免。怎么避免?往下看。

教训二:字段变更不是你想改就能改

API 里有个字段叫 name,产品说太土了,要改成 display_name。简单,搜索替换一下,上线。

然后灾难来了:

  • 旧版App解析不了 display_name,显示空白
  • 爬虫抓到的数据格式乱了
  • 某个第三方合作方的系统直接报错了

你以为的"小改动",对外部来说可能是灾难。

正确的做法是:字段只增不改,类型只宽不窄。

如果要重命名一个字段?分三步走:

// 第一步:保留旧的,同时返回新的(两个字段都有值)
{
  "name": "张三",
  "display_name": "张三",  // 新字段上线
}

// 第二步:等所有客户端都切换后,旧字段标记为 deprecated
{
  "name": "张三",  // @Deprecated 请迁移到 display_name
  "display_name": "张三",
}

// 第三步:下一个主版本再删除旧字段
{
  "display_name": "张三",
}

听起来很麻烦?但这是最小化线上事故的唯一方法。

教训三:错误处理的一致性,比你想象的更重要

我见过最离谱的API,错误返回是这样的:

// 情况1:用户不存在
{"error": "用户不存在"}

// 情况2:权限不足
{"message": "没有权限"}

// 情况3:服务器挂了
{"msg": "服务端异常"}

// 情况4:参数错误
{"info": "参数格式错误"}

四个错误,四种格式。调用方想统一处理?做梦。

我后来强制团队使用统一的错误格式:

{
  "code": 10001,
  "message": "用户不存在",
  "request_id": "abc123",
  "details": {}
}

code 是给程序用的,message 是给人类看的,request_id 是用来排查问题的。三者缺一不可。

还有一点:HTTP状态码和业务错误码是两回事。

很多新手喜欢用 200 然后在 body 里塞个 {"success": false}。这是错误的。HTTP 状态码 4xx 就是给客户端判断"我请求有问题"的,你用 200 糊弄过去,前端代码怎么写拦截?网关怎么做流量控制?

教训四:分页参数的设计,藏着大坑

最简单的分页是什么?

GET /users?page=1&size=20

很好,理解。但当你的数据量从一万条变成一亿条时,问题来了:OFFSET 分页是灾难。

-- 第1000页,每页20条
SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 20000;

-- 数据库要扫描前20020条,然后丢弃20000条

数据量越大,OFFSET 越大,性能越差。这是 SQL 本身的限制,不是你的代码问题。

正确的做法是游标分页(Cursor-based Pagination)

// 第一页
GET /users?limit=20

// 返回
{
  "data": [...],
  "next_cursor": "eyJpZCI6MTAwMH0=",  // 加密的最后一页最后一条的ID
  "has_more": true
}

// 下一页
GET /users?limit=20&cursor=eyJpZCI6MTAwMH0=

每次只查"从游标往后20条",不管数据量多少,查询时间恒定。这是 Instagram、Twitter 这些大厂都在用的方案。

教训五:你的API有没有做"防腐层"?

这个词很多人可能没听过,但问题很常见:你的内部数据模型和外部API暴露的数据模型,是两个东西。

很多小团队直接把我数据库表结构暴露给外部:

{
  "id": 1,
  "user_id": 123,
  "is_deleted": false,
  "create_time": "2024-01-01 10:00:00",
  "update_time": "2024-06-01 15:30:00"
}

然后某天你数据库字段改了,外部API也跟着变了,旧版本客户端全挂了。

防腐层(Anti-corruption Layer)的意思是:数据库怎么存是你的事,API怎么返回是我的事,中间加一层转换。

// 数据库模型
UserDB {
  id, user_id, is_deleted, create_time, update_time
}

// API模型(分离)
UserDTO {
  id, name, status, joined_at
}

// 转换逻辑
UserDTO.fromDB(UserDB) { ... }

这样做的好处是什么?你的数据库加字段、拆表、换存储引擎,API不用改。外部调用方永远看不到你内部的复杂性。

写在最后

API 设计没有银弹,但有一些可以让你少踩坑的原则:

  • 字段只增不改,类型只宽不窄
  • 错误格式统一,状态码正确使用
  • 分页用游标,别用 OFFSET
  • 内外模型分离,加上防腐层
  • 版本号是最后手段,不是首选方案

最重要的一点:设计API的时候,要假设你的调用方是"熊孩子"——他们会用你想不到的方式调用,会传你想不到的数据,会在你意想不到的时候升级或降级。

你能做的,就是让API足够健壮,足够宽容,足够稳定。

毕竟,好的API是沉默的——它只在出错的时候才开口,而出错的时候,它说的话你一定要能听懂。

相关文章

为什么你的接口总被重复请求坑死?一次说清幂等性设计
SQL优化:从30秒到30毫秒,我踩过的那些坑
Go的GC:那些教科书永远不会教你的骚操作
sync.WaitGroup:我以为我懂它,直到写出了生产事故
还在为部署AI工具秃头?来找小龙虾,39块搞定一切!
连接池泄漏:那个让我凌晨三点写检讨的bug,比失恋还难受

发布评论