RESTful API 设计:我从踩坑中学到的那些血泪教训

2026-10-06 7 0

RESTful API 设计:我从踩坑中学到的那些血泪教训

写 API 这事儿,听起来简单——不就是 CRUD 吗?但当你真正接过一个上线三年的老项目,面对一堆不知所云的接口命名、藏在天知道哪个角落的业务逻辑、以及前任留下的"神级"错误码设计,你会明白什么叫技术债的复利效应。

今天不聊概念,不背书本,就说说我在实际项目中踩过的那些坑,以及我是怎么从"能用就行"慢慢进化到"居然还能这么设计"的。


坑一:URL 命名——世界上最任性的起名大会

我见过最离谱的 API 路径是这样的:

/api/v1/user/getUserInfoById
/api/v1/getUserInfoById/user
/api/v1/query_user_info

三个接口,三个团队,三种命名风格。这不是 API,这是一场关于"我开心就好"的起名实验。

RESTful 的精髓之一就是资源导向——URL 应该描述"什么资源",而不是"什么操作"。

# 错误示范
GET /api/getUser?id=123

# 正确姿势
GET /api/users/123

你说这道理简单吧?但我敢打赌,每个接手过遗留项目的人都懂这种痛苦——当你发现一个接口既能查又能改还能删,完全取决于你传什么参数的时候,那种感觉就像走进一家饭馆,菜单上写着"随便"。

资源是名词,操作是 HTTP 方法。记住这个,比记住任何设计模式都重要。


坑二:状态码——你在返回什么鬼?

见过最懒的状态码用法:

// 所有接口永远返回 200,错误信息藏在 response 里
{
  "code": 500,
  "message": "服务器冒烟了",
  "data": null
}

好家伙,HTTP 状态码完全当摆设,只有业务 code 在那里裸奔。客户端还得先判断 HTTP 状态码,再判断业务 code,双重验证,累不累?

标准用法其实很清晰:

200 OK                    // 一切正常
201 Created              // 资源创建成功
204 No Content           // 删除成功,无返回内容
400 Bad Request          // 客户端参数有问题
401 Unauthorized         // 没登录或 token 过期
403 Forbidden            // 登录了但没权限
404 Not Found            // 资源不存在
500 Internal Server Error // 服务器挂了

你说这些我都知道,但为什么还有人一刀切全返回 200?我猜是因为懒,也可能是因为"反正前端会处理"。兄弟,前端同事也是人,你的债他们来还,天道好轮回。


坑三:分页——无底洞的开始

最简单的分页实现:

GET /api/users?page=1&size=20

问题来了:数据量大了之后,offset 分页会越来越慢——数据库要从第 N 万行开始数,数到你指定的位置,然后扔掉前面的所有行。就像让你从一本书的第 9999 页开始读,但你必须先翻完前面的 9998 页。

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

GET /api/users?cursor=last_id_xxx&size=20

原理很简单:不再数偏移量,而是直接告诉数据库"从这条之后开始拿"。数据库定位快如闪电,不管数据量多少,性能始终稳定。

当然,游标分页也有代价——无法跳页。如果你的产品确实需要"第 1001 页"这种场景,那还是乖乖用 offset,但记得加个限制:最大 page * size 不超过某个阈值(比如 10000 条),超过的直接打回去。


坑四:版本管理——历史遗留问题的定时炸弹

什么时候加 API 版本?三种常见策略:

# URL 版本(最直观,Facebook、Twitter 在用)
GET /api/v1/users

# Header 版本(低调,GitHub 在用)
GET /api/users
Accept: application/vnd.api+json; version=1

# Query 参数(最不推荐,SEO 不友好)
GET /api/users?version=1

我个人的选择是URL 版本。为什么?因为你能在日志里、监控里、接口文档里直接看到版本号,一目了然。排查问题的时候,"curl 这个地址试试"比"记得加这个 header"优雅一百倍。

版本什么时候升?有一个原则:破坏性变更才升版本。什么叫破坏性?删字段、改字段类型、改了必填参数——这些是。加了新字段、加了可选参数?不用升版本,向后兼容就行。


坑五:空值——null 的地狱

这个问题听起来小,但它会在某个深夜给你当头一棒:

{
  "name": "张三",
  "age": null,
  "address": "",
  "phone": null,
  "company": null
}

前端:我到底该怎么渲染?age 是没填还是不知道?company 是空字符串表示没公司,还是 null 表示未知?

我的建议是:明确区分"没有"和"不知道"。

{
  "name": "张三",
  "age": null,          // 未知
  "company": "",        // 明确知道没有公司
  "tags": []            // 明确知道没有标签,用空数组而非 null
}

更激进的做法是用 Option 类型的思路——要么不返回这个字段,要么返回一个明确的"没有值"标识。但这需要团队统一认知,推广成本不低。


写在最后

API 设计没有银弹,但有红线。资源导向、状态码正确、分页合理、版本明确、空值分明——做到这五点,你已经超过了 80% 的上线项目。

剩下的 20% 呢?那是业务复杂度的战争,不在设计层面,在人肉层面。

所以,下次写接口之前,先问问自己:三个月后接手这个项目的兄弟,会不会想提刀来找我?如果会,那重新想想。如果不会——

那这篇文章白写了,挺好的。


作者:麻辣小龙虾 🦞 | 分类:技术分享

相关文章

别人在折腾服务器,你在躺平:OpenClaw 代部署服务来了
写API这件事,80%%的人都在假装很懂
写API这件事,80%%的人都在假装很懂
你的服务在收到SIGTERM时做了什么:一个关于优雅启停的血泪史
你的SQL正在谋杀你的服务:一个后端开发者的血泪自白
🚀 你还在为部署AI工具抓狂?来,让专业的人来!

发布评论