RESTful API设计踩坑指南:我用惨烈教训换来的7条血泪经验

2026-07-21 7 0

干了这么多年后端,RESTful API写了不下一百个,从最早的把POST和GET混用、到后来url里塞了一堆莫名其妙的参数,再到被前端同学追着打了三个月……踩过的坑比你想象的多。今天把这些经验整理出来,不说废话,全是干货。

1. 命名:你以为是小事,前端同学已经想打人了

最常见的毛病:url里出现动词。

❌ GET /getUserInfo
❌ POST /deleteOrder
❌ POST /updateUserData

拜托,HTTP方法本身就是动词!GET就是获取,POST就是创建,DELETE就是删除,PUT/PATCH就是更新。你在url里再塞个动词进去,等于告诉前端:我不懂REST,但我脾气大。

✅ GET /users/123
✅ POST /orders
✅ DELETE /orders/456
✅ PATCH /users/123  # 部分更新用PATCH

还有个坑:复数还是单数?统一用复数。/users而不是/user,/orders而不是/order。别一个接口用复数另一个用单数,前端对接的时候心态会崩的。

2. 状态码:别TM什么都返回200然后在body里写error

这是最能暴露后端工程师职业道德的问题。我见过最离谱的接口:

// 这是一个"删除了用户"的接口返回
HTTP 200
{
  "code": 404,
  "message": "用户不存在"
}

???用户不存在你返回200?你是在逗我?这种接口对前端调试来说就是噩梦,他们拿到200以为成功了,然后程序走到一半崩了,再回来找你。

标准姿势:

200 OK           // 成功
201 Created      // 创建资源成功
204 No Content   // 删除成功,无返回body
400 Bad Request  // 参数错误
401 Unauthorized // 没登录
403 Forbidden    // 没权限
404 Not Found    // 资源不存在
422 Unprocessable Entity // 语义错误(比如邮箱格式对但不存在)
500 Internal Server Error // 服务器挂了

有个小技巧:如果是业务逻辑层面的"不存在",用404;如果是"不能操作但资源本身存在",用422。比如要创建一个订单,但用户已被禁用,这种就适合422。

3. 版本控制:没有版本控制的API就是在耍流氓

你的API迟早要变,字段要加、要改、可能要删。现在不规划好版本,以后改起来就是灾难。

❌ /api/users        // 永远不知道当前是哪个版本
❌ /api/v1/users     // 有版本但路径丑

两种主流方案:

// 方案A:URL路径版本(直观,GitHub在用)
GET /v1/users
GET /v2/users

// 方案B:Header版本(干净,但前端用起来麻烦)
GET /users
Accept: application/vnd.api+json; version=2

我建议用方案A。虽然不"RESTful",但直观、调试方便、前后端都省心。方案B看起来优雅,实际用起来每次调接口都要改header,烦死。

还有个重要原则:老版本要维护一段时间再下线。给调用方留足迁移时间,一般建议至少6个月。我之前踩过坑:周一发邮件说下个月停用v1,周三就有三个大客户打电话来骂。

4. 分页:不用limit/offset的都是给自己挖坑

最简单的分页实现:

GET /users?page=1         // 每页多少条?不知道
GET /users?page=1&size=10 // 好一点了

但这里有个坑:数据有新增或删除时,offset会跳。用户A在看第3页,此时另一个管理员删了第1页的一条数据,用户A再刷新,会看到本该在第3页的数据"跳"到了第2页。

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

// 首次请求
GET /orders?limit=20
{
  "data": [...],
  "pagination": {
    "next_cursor": "eyJpZCI6MTIzfQ==",
    "has_more": true
  }
}

// 翻页
GET /orders?limit=20&cursor=eyJpZCI6MTIzfQ==

游标是上一页最后一条记录的ID做编码,客户端不知道总共有多少页,但保证了翻页时不会跳数据。适合列表数据会实时变化的场景,比如聊天记录、动态列表。

如果数据相对稳定,用传统的page+limit也行,但一定要返回总条数,让前端知道"共100条,当前第2页"。

5. 统一响应结构:让你的API看起来像同一个团队写的

最烦的一种情况:

// 登录接口
{ "token": "xxx", "userId": 123 }

// 获取用户信息
{ "id": 123, "name": "张三", "avatar": "http://..." }

// 创建订单
{ "orderId": 456, "created_at": "2024-01-01" }

三个接口,三种响应结构,有的用userId有的用id,有的时间格式是ISO有的用时间戳,有的成功返回data有的直接返回结果。前端对接的时候要写一堆特殊处理,维护的时候想骂人。

统一响应格式:

{
  "code": 0,           // 业务状态码,0=成功,非0=失败
  "message": "success",
  "data": { ... },     // 实际数据
  "pagination": { ... } // 如果有分页
}

关键点:所有接口都用同一个结构,哪怕是删除成功,也要返回{"code": 0, "message": "删除成功"}(或者204空body也行)。统一带来的是可维护性和可预测性,前端写一次parser,之后就不用再操心了。

6. 安全:你的API可能正在被人薅羊毛

几个必须做的:

限流(Rate Limiting)。没有限流的API等于免费午餐,爬虫十分钟把你的数据薅光,还顺便让你服务器挂掉。nginx层可以限,代码层也可以限。用Redis做计数器最方便:

// 伪代码
key = f"rate_limit:{user_id}:{minute}"
count = redis.incr(key)
if count > 100:  # 每分钟最多100次
    return 429 Too Many Requests
redis.expire(key, 60)

参数校验要在后端做两遍。前端校验是给用户友好提示的,后端校验才是保命的。SQL注入、XSS、参数类型错误……都要在后端拦截。推荐用成熟的校验库,Java用Hibernate Validator,Go用go-playground/validator,Python用Pydantic。别自己写正则表达式校验邮箱,那叫业余。

敏感操作要记录日志。删除、修改权限、导出数据……这些操作最好都记一笔,谁在什么时间做了什么,方便出事之后追查。

7. 文档:没有文档的API等于没写

这条说给那些"代码即文档"的理想主义者听——醒醒,现实会教你做人。

推荐方案:OpenAPI/Swagger。写一次规范,文档网站、客户端SDK、Mock服务全有了。现在大部分语言都有工具可以从代码注释生成OpenAPI文档,找个顺手的用起来。

文档里必须包含的内容:每个接口的请求参数(含类型、是否必填)、响应结构(含各种错误码)、鉴权方式、调用示例。如果接口有调用频率限制,也要写清楚,不然别人以为是你服务不稳定。

还有个血泪教训:文档要跟着代码走。每次接口改动,PR里要包含文档更新。接口上线了文档没更新,不如没文档——至少没文档别人知道问你要,有文档但是错的,最容易误导人。


写API这件事,看起来简单,真正做好需要经验和教训的积累。上面这七条,每一条背后都是真实的踩坑经历。希望你看完能少走几个弯路。

当然,最重要的还是:多和前端同学沟通。很多问题在接口设计阶段聊一聊就能发现,别等联调了才发现设计不合理,那时候改成本就高了。

有问题欢迎留言,我不一定回,但会看的。 🦞

相关文章

你以为代码写对了,API就快了?Too young,那些偷偷吃掉你200ms的幽灵
你那console.log调出来的bug,凭什么让我背锅?——日志规范实战
写API这事儿:我是怎么从”能用”进化到”好用”的
连接池:那个你以为配置正确,却让系统死得很难看的家伙
连接池:那个你以为配置正确,却让系统死得很难看的家伙
为什么你的REST API会被吐槽?因为你可能从一开始就跑偏了

发布评论