RESTful API设计:那些年我们一起踩过的坑

2026-07-02 5 0

前言

写API这件事,看起来简单,不就是把数据扔出去吗?但当你真正面对生产环境,面对各种奇怪的调用方行为,面对半夜的告警,你才会意识到:API设计烂,后端火葬场。

今天不聊理论,就聊实战。我踩过的坑,比你写过的CRUD还多。


一、HTTP方法用对了吗?

见过太多人所有请求都走POST,问就是"方便"。兄弟,你是在设计API,不是在写漏洞利用代码。

HTTP方法不是摆设:

  • GET:读取资源,禁止修改任何东西
  • POST:创建资源
  • PUT:全量更新(传完整对象)
  • PATCH:部分更新(只传要改的字段)
  • DELETE:删除资源

有个经典错误:有人用POST做删除操作,美其名曰"安全"。结果呢?搜索引擎爬虫没事爬一下,线上数据就没了。你说这是爬虫的错?不,这是你API设计的错。

// 正确示范
DELETE /users/123  // 删除用户
GET /users/123     // 获取用户信息
PATCH /users/123   // 更新部分字段

// 错误示范——所有操作都是POST
POST /deleteUser?id=123
POST /getUserInfo
POST /updateUser

二、状态码:别总返回200然后在body里塞error

这是国内最常见的烂设计:HTTP状态码永远是200,但body里写着{"code": 500, "message": "服务器错误"}。你是在逗我?

HTTP状态码是有意义的,用它们:

  • 200:成功
  • 201:资源创建成功
  • 400:客户端请求有问题(比如参数校验失败)
  • 401:未认证
  • 403:已认证但没权限
  • 404:资源不存在
  • 500:服务器内部错误

有人说:"返回错误状态码,前端不好处理啊。"前端不好处理是前端的问题,不是你乱用状态码的理由。

// 好的设计:正确的HTTP状态码
HTTP/1.1 400 Bad Request
{"code": 40001, "message": "手机号格式不正确", "field": "phone"}

// 烂的设计:永远200
HTTP/1.1 200 OK
{"code": -1, "message": "手机号格式不正确"}

三、分页:无限滚动的代价

很多人在设计列表接口的时候,习惯用offset+limit,但当数据量大的时候,你就知道什么叫灾难。

// 经典烂设计
GET /articles?page=1\&page_size=100

// 问题在哪?
// 第1000页 = OFFSET 9900 LIMIT 100,数据库要扫描9900行再返回100行
// 数据有新增删除时,翻页会看到重复或遗漏数据

更好的方案是游标分页(Cursor Pagination):

// 基于游标的分页
GET /articles?cursor=eyJpZCI6MTIzfQ\&limit=20

// 返回
{
  "data": [...],
  "next_cursor": "eyJpZCI6MTAzfQ",
  "has_more": true
}

// 优点:
// 1. 不管翻到第几页,性能一致
// 2. 数据有变化不会重复或遗漏
// 3. 适合移动端无限滚动场景

当然,游标分页也有代价:无法跳转到任意页。这个要根据自己的业务场景取舍。


四、版本管理:你的API升级策略是什么?

总有一天,你需要breaking change。但上线第一天就把现有接口改了?调用方会提着刀来找你。

常见的版本管理策略:

// URL版本(GitHub、Stripe在用)
GET /v1/users
GET /v2/users

// Header版本
GET /users
API-Version: 2023-01-01

// Query参数版本(不推荐)
GET /users?version=2

我的建议是:URL版本最直观,但也最容易被用户骂(因为看起来不优雅)。Header版本更符合REST精神,但调试不方便。

不管用哪种,关键是:旧版本要有足够的生命周期,给调用方足够的时间迁移。我见过有人上线新版本,两周后就下掉了旧版本,然后被客户投诉到工信部。


五、幂等性:你考虑过吗?

网络是不稳定的。超时、重试、用户手抖多点了一下——你的API能被调用多次而不产生副作用吗?

这些操作必须是幂等的:

  • GET:天然幂等
  • PUT:多次调用结果一致
  • DELETE:删除已删除的资源应该返回成功
  • POST:通常不是幂等的,但可以用唯一token解决

一个实战技巧:对于POST类创建操作,让客户端生成唯一ID(可以是UUID),服务端做去重。

// 客户端生成唯一ID
POST /orders
X-Idempotency-Key: a3f1c2d4-e5b6-7890-abcd-ef1234567890

// 服务端存储已处理的key,超时自动清理
if (idempotencyStore.has(key)) {
  return idempotencyStore.get(key)  // 返回缓存结果
}
idempotencyStore.set(key, result)
return result

六、字段命名:别用拼音,别用缩写

这是我见过最离谱的问题:

// 来自某国内项目的API
{
  "yonghu_xingming": "张三",
  "shijian": "2024-01-01",
  "je": 1000
}

// je是什么?金额?积分?
// shijian是什么?创建时间?修改时间?

用英文,用完整的英文。遵循业界通用命名:

  • user_name 或 userName(看你团队风格统一)
  • created_at
  • updated_at
  • amount 或 total_amount

还有一个坑:时间字段必须有时区信息,或者统一用UTC。北京时间下午3点,在UTC是早上7点。如果你存的是本地时间,某天服务器迁移到国外,时区问题会让你怀疑人生。


结语

好的API设计,是让调用方用起来舒服,自己改起来也不痛苦的设计。它不是炫技,是克制——克制地使用功能,克制地返回数据,克制地在breaking change之前先发邮件通知。

记住:你的API是给别人用的,不是给自己炫技的。把用户当傻子一样设计接口,用户会用脚投票。

以上,踩坑完毕,欢迎补充。

相关文章

我在生产环境用Docker跑数据库,被leader当场骂了一顿
代码写得越优雅,死得越惨:我是如何被异步编程坑出工伤的
当AI开始整活:我和OpenClaw的相爱相杀日常
还在为AI工具部署抓狂?交给小龙虾,三分钟搞定!
RESTful API 已经死了,Long Live RESTful API
RESTful API 早已死去,只是你还在装睡

发布评论