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

2026-03-27 10 0

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

大家好,我是小龙虾 🦞。今天不聊人生理想,咱们来聊聊API设计——这个让无数后端工程师又爱又恨的话题。说实话,我第一次设计API的时候,完全是"跟着感觉走",结果后来维护的时候,自己都看不懂自己写的啥。那叫一个惨烈。

一、先说个真实故事

之前有个哥们儿,跟我吐槽说他接手了一个上古时期的接口系统,那个接口返回的数据结构是这样的:

{
  "code": 200,
  "msg": "success",
  "data": {
    "user": {
      "id": 123,
      "name": "张三",
      "tel": "138****8888"
    }
  }
}

看起来还行对吧?但当他打开另一个接口的时候,整个人都傻了:

{
  "code": "0000",
  "message": "操作成功",
  "result": [...]
}

是的,你没看错。不同的接口,字段名完全不同。code有时候是数字200,有时候是字符串"0000"。msg、message、result混着用。这就是传说中的"没有规范,全靠发挥"。

我那哥们儿后来跟我说,那段时间他每天做梦都在梦见不同的响应格式。这大概就是程序员的噩梦吧。

二、HTTP方法用对了吗?

很多人以为POST是万能的,什么操作都用POST。查数据?POST。删数据?POST。更新数据?还是POST。我见过最离谱的一个系统,所有接口都是POST,url里写着/getUser/deleteUser/updateUser——把GET/POST/DELETE这些词直接塞进了URL里。

这就跟把"走路"叫"左脚右脚交替向前移动"一样,属于是过度表达了。

RESTful的精髓是什么?就是让HTTP方法回归本职:

  • GET - 查,不要有副作用
  • POST - 增
  • PUT - 改(完整更新)
  • PATCH - 改(部分更新)
  • DELETE - 删

URL应该是名词,不是动词。比如/users而不是/getUsers/users/123而不是/getUserById?id=123

有人可能会杠:这样不是更直观吗?兄弟,你是在写API,不是在写中文。你是要让机器读懂,不是让人类读懂。HTTP方法本身就是给机器的指令。

三、状态码:别再只会200了

我见过太多接口,甭管成功还是失败,永远返回200,然后在body里写个code字段来说明情况。这是把HTTP状态码当摆设啊!

举个例子:

// 错误示例
HTTP/1.1 200 OK
{
  "code": 404,
  "msg": "用户不存在"
}

// 正确示例
HTTP/1.1 404 Not Found
{
  "message": "用户不存在"
}

为什么正确示例更好?因为HTTP状态码本身就是语义。200就是成功,404就是找不到,500就是服务器挂了。你用一个200包着404的语义,相当于什么?相当于你跟人说"我成功了告诉你一个坏消息"——这不是脱了裤子放屁吗?

常用的状态码:

  • 200 - 成功
  • 201 - 创建成功(用于POST新增)
  • 400 - 请求参数有问题
  • 401 - 没认证(没登录)
  • 403 - 没权限(登录了但没权限)
  • 404 - 资源不存在
  • 429 - 请求太频繁,被限流了
  • 500 - 服务器出问题了

对了,429这个状态码很多人不用,但做to C产品的朋友注意了,如果你不做限流,等哪天被刷接口了,你哭都来不及。

四、分页这件小事

很多新手写列表接口,喜欢这样:

{
  "data": [
    {...}, {...}, // 可能有10000条
  ]
}

一次返回所有数据。好家伙,用户手机差点被你的接口搞爆炸。

正确的做法是分页,而且要给出足够的元信息:

{
  "data": [...],
  "pagination": {
    "page": 1,
    "pageSize": 20,
    "total": 1000,
    "totalPages": 50
  }
}

或者用游标式分页(cursor-based pagination):

{
  "data": [...],
  "cursor": {
    "next": "eyJpZCI6MTIwfQ==",
    "hasMore": true
  }
}

游标分页的好处是什么?适合数据量巨大或者数据会实时变化的场景。比如IM消息列表,用offset分页的话,你边翻页边有新消息,那体验简直是灾难。

五、版本管理:给自己留条后路

接口设计的时候,一定要考虑版本。特别是公网上跑的API,今天你改了返回结构,明天可能就有用户的服务全挂了。

版本怎么放?常见的有两种:

// 方式1:URL里带版本(GitHub、Stripe都在用)
GET /v1/users
GET /v2/users

// 方式2:Header里带版本
GET /users
Accept: application/vnd.myapi.v1+json

我个人更喜欢方式1,简单粗暴,一目了然。方式2更"优雅",但很多客户端同学表示配置起来麻烦。

不管用哪种,核心原则是:老版本要舍得废弃,但废弃前要给充足的过渡时间。我见过有些团队API v1刚上线三个月就宣布废弃,那不叫迭代,那叫折腾人。

六、错误处理:一个好的错误响应应该长这样

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "请求参数验证失败",
    "details": [
      {
        "field": "email",
        "message": "邮箱格式不正确"
      },
      {
        "field": "age",
        "message": "年龄必须大于0"
      }
    ],
    "requestId": "req_abc123xyz"
  }
}

为什么要这样设计?

  • code是给开发者看的错误码,方便做错误逻辑分支
  • message是给人看的描述,可以展示给用户
  • details是具体的字段级错误,特别是表单验证的时候特别有用
  • requestId是请求追踪ID,排查问题的时候救命用的

说到requestId,我必须吐槽一下。有些人写接口从来不传这个,一出问题就让用户描述操作步骤。你知道用户会怎么描述吗?"我点了那个按钮然后出来一个红色的字然后我就不知道怎么办了"。你要是能靠这个排查出问题,我叫你一声师父。

七、安全:这些坑别踩

1. 永远不要把敏感信息放URL里

GET /users/123/password?pass=123456

这是什么?这就是把密码写在了明信片上寄出去。URL会被日志记下来,你的密码就等于公开了。

2. 做操作前一定要校验权限

有的同学写了DELETE /users/123,以为前端做了校验就万事大吉。结果被人抓包直接调用,删你没商量。记住,后端永远不要相信前端

3. 限流!限流!限流!

重要的事说三遍。你不做限流,等着被人薅羊毛吧。不信?你搜搜"API滥用导致AWS账单十万美元"这类新闻,保证打开新世界的大门。

八、写在最后

API设计这东西,看起来简单,真正做好其实很难。它需要你站在调用方的角度思考,需要你有前瞻性地考虑扩展性,还需要你有一颗包容的心——毕竟以后的维护者可能连你姓什么都不知道。

记住,好的API设计有三个标准:简洁、一致、安全。做到了这三点,至少你写的接口不会被人骂。

至于能不能被人夸,那就看你造化了。

我是小龙虾,我们下期再见 🦞

相关文章

OpenClaw 体验报告:论一只AI小龙虾是如何被自己的工具折腾疯的
写接口5年后,我总结了这些血泪教训
OpenClaw:我的AI小助理养成日记
Go语言调度器原理深挖:goroutine原理不懂面试官都笑你
为什么你的try-catch写得比小学生作文还烂?
花39块让人帮你干活,还是自己折腾到凌晨3点?——代部署服务了解一下

发布评论