写API接口这事儿,80%%的人都在假装REST

2026-08-25 11 0

我对接过无数第三方API,每次都觉得自己在上辈子造了孽。

你们有没有那种感觉:文档看了三遍,接口调不通,错误信息写的是操作失败,然后你去找技术支持,人家说"你查一下日志"。

我查你个大爷。

今天不吐不快,把这些年见过最离谱的API设计问题全抖出来,附带解决方案。收藏了以后面试都能用。


一、URL设计:你的动词放哪儿去了?

最常见的迷惑行为:一个接口长这样

POST /api/getUserInfo
POST /api/addNewProduct
POST /api/deleteOrderById

兄弟,HTTP方法你是一个都不认识吗?

GET是查,POST是增,PUT是改,DELETE是删,这是基本常识。但凡你学过一天RESTful都不会这么干。

正确的打开方式:

GET    /users/123
POST   /users
PUT    /users/123
DELETE /users/123

名词复数,动词靠HTTP方法。这不是最佳实践,这是基本礼仪。

但问题来了——为什么这么多人还在用动词型URL?

因为很多后端框架的路由设计太直白了,/api/getUser写起来顺手,老板问起来也好解释。但这种设计本质上是把HTTP当传输管道用,完全浪费了协议本身的能力。

更深的问题是:当你用动词URL的时候,你的接口很难做统一的权限控制、缓存和日志。你没法对所有GET请求统一加缓存策略,因为有些"查询"藏在POST里。

所以我的建议是:先把HTTP方法用对,再谈别的。这是最基本的。


二、错误处理:你的400是认真的吗?

让我来猜猜你们公司接口的错误响应长什么样:

{
  "code": 400,
  "message": "操作失败",
  "data": null
}

好,现在告诉我:这个400是什么意思?是参数校验失败?还是用户不存在?还是服务器数据库崩了?

你猜,你使劲猜。

很多接口的错误响应完全没信息量,message永远是"操作失败",code永远是那个数字。你根本不知道发生了什么,只能一遍遍试。

优秀的错误设计应该长这样:

{
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "用户不存在,请检查user_id是否正确",
    "details": {
      "field": "user_id",
      "provided_value": "abc123",
      "reason": "格式不符,长度应为10位数字"
    },
    "request_id": "req_7f8a9b2c3d"
  }
}

这个响应告诉你:问题是什么,在哪个字段,为什么,怎么追踪。

错误码要业务化,不要用技术状态码代替业务语义。USER_NOT_FOUND404有用一百倍,因为前者直接告诉你"用户不存在",后者你还得猜404是哪个资源。

另外,HTTP状态码要合理使用:

  • 400:客户端参数错误
  • 401:未认证
  • 403:已认证但无权限
  • 404:资源不存在
  • 422:语义正确但业务不允许
  • 429:请求太频繁
  • 500:服务端问题

不要所有错误都返回200然后在body里塞个success: false。这种设计让HTTP状态码完全失去意义,监控报警都做不好。

你说"我们返回200是方便前端统一处理"——那你方便了,nginx日志和监控报警工具找谁哭去?


三、响应结构:一会儿数组一会儿对象

这是另一个高频坑。同一个接口,有时候返回数组,有时候返回对象。

// 查一个
{
  "id": 1,
  "name": "产品A"
}

// 查多个
[
  {"id": 1, "name": "产品A"},
  {"id": 2, "name": "产品B"}
]

前端看到这个要骂人的。

统一响应结构是API设计的基本功。我的推荐方案:

// 查单个也包装成数组风格(推荐)
{
  "data": {
    "id": 1,
    "name": "产品A"
  },
  "code": 0
}

// 列表用 pagination
{
  "data": [
    {"id": 1, "name": "产品A"},
    {"id": 2, "name": "产品B"}
  ],
  "pagination": {
    "page": 1,
    "page_size": 20,
    "total": 100,
    "total_pages": 5
  },
  "code": 0
}

所有响应都统一根级别结构:data放业务数据,code表示状态,message放错误信息。

这有什么好处?前端可以写一个统一的响应拦截器,所有接口的错误处理和loading状态都可以复用。你的代码量直接少一半。


四、Pagination:为什么你的分页这么难用

很多接口的分页设计是这样的:

GET /users?page=1&size=20

然后返回:

{
  "users": [...],
  "total": 1000
}

这个设计看起来没问题,但实际用起来要命:

第一,total字段在深层嵌套里,前端每次都要response.data.users这样访问,多层嵌套看多了头疼。

第二,没有下一页的标记。你不知道total_pages,只能自己算:Math.ceil(1000/20)=50,然后判断page>=50就说明到底了。服务端万一悄悄提高了limit,你算出来的边界全是错的。

第三,最致命的——没有游标(cursor)。当你数据在第二页和第三页之间被删除或新增时,页码式分页会出现数据错位。用户可能看到重复数据,或者漏掉一些记录。

更优的分页设计用cursor:

GET /users?cursor=eyJpZCI6MTAwfQ==&limit=20

返回:

{
  "data": [...],
  "pagination": {
    "next_cursor": "eyJpZCI6MTIwfQ==",
    "has_more": true
  }
}

cursor是上一页最后一条记录的加密ID,下一页请求直接带这个cursor。这样不管数据怎么变化,分页结果都是稳定的。

当然,cursor分页不适合随机跳页场景。如果你的业务需要"第37页"这种精确访问,页码分页也可以接受,但一定要返回total_pages而不是让客户端自己算。


五、版本管理:你的v1什么时候是个头?

很多项目一开始就这样设计:

/api/v1/users
/api/v1/products

然后v2遥遥无期,v1跑了五年。

问题来了:你当初设计v1的时候,很多东西没想清楚,现在要改字段语义,怎么办?

几种常见策略:

URL版本(最常见):/api/v1/users/api/v2/users

优点:直观。缺点:维护两套代码,v1还在被使用就不能删。

Header版本Api-Version: 2024-01-01

优点:URL干净。缺点:调试不方便,CDN缓存也麻烦。

演进式:字段只增不改,旧字段标记deprecated但保留

优点:不用频繁开新版本。缺点:响应体会越来越臃肿。

我的经验:URL版本是最实用的。虽然不完美,但好维护、好调试、好让第三方知道他们在用什么版本。

更重要的是——不要在接口里暴露技术细节。数据库表名、内部字段名、能推导出其他接口的线索……这些都不应该出现在URL或响应里。API是对外的契约,不是内部实现的黑板报。


写在最后

API设计这件事,说到底是对调用者的尊重。

你的接口是给别人用的。人家在凌晨两点对着你的文档调接口的时候,如果错误信息写得像"网络异常请稍后再试"这种废话,人家心里一定在想:写这破接口的人是不是脑子有问题。

不想被骂,就好好做:错误信息要具体,分页要健壮,HTTP状态码要用对,文档要写清楚。不要觉得"反正能跑就行",你糊弄接口,接口就糊弄你——迟早的事。

以上。祝大家的API都能一次调通。

相关文章

你的HTTP客户端正在偷偷”饿死”你的服务——一个被忽视的性能杀手
还在为部署AI工具掉头发?来,让专业的人干专业的事 🦞
RESTful API 设计翻车现场:我从血泪中总结的避坑指南
一次诡异的死锁,让我发现了MySQL MVCC最深处的秘密
RESTful API设计中的七宗罪,看看你踩了几个
别再自己折腾了,让我帮你一键部署 AI 工具 🚀(¥39起)

发布评论