RESTful API 设计指北:我踩过的那些坑和总结的套路

2026-07-07 7 0

做后端开发这么多年,写过的 API 没有一千也有八百。从最开始的「能跑就行」,到后来的「这个接口设计得真烂我自己都看不下去」,再到现在的「有点东西」,中间踩过的坑够我写一本血泪史。今天不整虚的,直接上干货,说说那些真正有用的 API 设计套路。

一、先搞清楚你是在给谁写接口

很多新手拿到需求就开始写,写完了发现移动端要的数据和 PC 端不一样,然后就开始疯狂打补丁。一个接口走天下?迟早把自己走死。我的经验是:按客户端维度拆分接口,而不是按业务逻辑堆在一起

举个例子,有个电商系统,最开始只有一个商家后台,接口都是给 PC 端用的。后来上了 APP,然后又上了小程序,还有供应商入驻的独立后台。结果呢?一套接口打天下,每次改一个端的需求,其他端跟着遭殃。这种耦合简直让人崩溃。

正确的做法是:API Gateway 后面按客户端部署独立的服务。BFF 模式(Backend For Frontend)不是银弹,但在多端适配这件事上,确实能救命。

二、URL 设计:名词为王,别整动词

这个问题我见过无数次了:

POST /api/getUserInfo
POST /api/addNewProduct
POST /api/deleteOrderById
GET /api/updateUserData

兄弟,你这不叫 RESTful,你这叫「用 HTTP 方法的壳子包装 RPC 的灵魂」。REST 的精髓是什么?用名词表示资源,用 HTTP 方法表示操作

正确的打开方式:

GET /api/users/123        # 获取用户信息
POST /api/users           # 创建用户
PUT /api/users/123        # 更新用户
DELETE /api/users/123     # 删除用户
PATCH /api/users/123      # 部分更新用户

PATCH 和 PUT 的区别很多人搞不清楚。简单说:PUT 是全量替换,PATCH 是部分更新。比如更新用户头像,PATCH 就比 PUT 优雅得多——你不需要传一整坨用户对象。

三、状态码:别一成功就返回 200

我见过最离谱的接口是:不管发生什么,返回永远是 200,然后 success 字段 true 或 false。这完全浪费了 HTTP 状态码的意义。

状态码就是接口的语言,你的合作伙伴(前端、移动端、其他服务)都在听这门语言。你不能说「成功了」,然后在消息体里悄悄说「其实失败了」。这不是欺骗是什么?

我的状态码规范:

200 OK              # 查询成功、更新成功
201 Created         # 资源创建成功,响应头带上 Location
204 No Content      # 删除成功,无响应体
400 Bad Request     # 参数校验失败,附上错误详情
401 Unauthorized    # 没登录或 token 过期
403 Forbidden       # 登录了但没权限
404 Not Found       # 资源不存在
409 Conflict        # 资源冲突,比如用户名已存在
422 Unprocessable   # 语义错误,参数格式对但逻辑不对
429 Too Many Request # 请求过于频繁
500 Internal Error  # 服务器炸了

重点说下 201 和 204。创建资源返回 201 是不成文的规定,同时在响应头加上 Location: /api/users/456,告诉调用者新资源在哪。删除操作返回 204 是因为删除操作通常没有响应体,你总不能返回一个「删除成功」吧?

四、错误响应:给前端一条活路

错误响应这件事,做得好不好直接决定前端的开发体验。我见过太多接口返回这种鬼东西:

{
  "code": 500,
  "message": "服务器内部错误",
  "data": null
}

500?服务器内部错误?这信息量等于零。前端同学看到这个只能对着屏幕发呆,连下手改的地方都没有。

我的错误响应结构:

{
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "指定的用户不存在",
    "details": [
      {
        "field": "user_id",
        "message": "用户 ID 格式正确但数据库中不存在"
      }
    ],
    "trace_id": "a1b2c3d4e5f6"  // 这个很重要,后面会提到
  }
}

code 是给程序看的,用大写下划线命名,客户端好做 switch-case。message 是给用户看的,可以是中文。details 字段放具体哪个字段出错及原因,方便前端精准渲染到表单上。trace_id 是链路追踪 ID,出问题时一搜就知道是哪台机器、哪个请求的锅。

五、分页:别一次性全量返回

「这个接口怎么这么慢?」「你猜它返回了多少数据?」「不知道,反正订单表有几千万条。」

全量返回这种操作,在测试环境看起来没问题,一上生产就是灾难。分页是必须的,但分页的实现方式也有讲究。

常见的分页方式有两种:offset 分页cursor 分页(也叫游标分页)。

offset 分页:

GET /api/orders?page=1&page_size=20

简单粗暴,但有性能问题。数据库执行 OFFSET 10000 LIMIT 20 时,得先扫描前 10020 行然后丢弃前 10000 行,数据量大的时候慢得离谱。

cursor 分页:

GET /api/orders?cursor=eyJpZCI6MTAwMH0&limit=20

基于上一页最后一条的 ID 做条件查询,WHERE id > last_id LIMIT 20,不管翻到第几页,性能都稳定。但 cursor 分页不支持跳页,用户想直接跳到第 50 页?不好意思,做不到。

我的建议:数据量小用 offset,数据量大或列表有实时性要求用 cursor。比如订单列表、消息列表这种,用 cursor 分页体验反而更好——用户往下滑,订单一直在变,用 offset 分页翻到第 5 页可能已经看到过期订单了。

六、版本管理:向前兼容是基本礼仪

接口上线后不可能不改,但改了不能影响已有的调用方。版本管理就是干这个的。

两种常见方式:

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

// Header 版本(AWS 在用)
GET /api/users
Accept: application/vnd.api+json; version=2

我更推荐 URL 路径版本,原因是看得见摸得着。调用方可以明确知道自己用的是哪个版本,出问题时好排查。Header 版本的好处是 URL 干净,但调试的时候得记着带 header,相对麻烦。

另外,接口废弃要给出明确的过渡期。Stripe 的做法值得学习:废弃接口会返回 Deprecation: true 响应头,并给出 Sunset 响应头说明具体废弃时间。调用方能看到预警,有充足时间迁移。

七、安全:别等被刷才知道后悔

API 安全是个大话题,说几个最容易忽略的点:

1. 限流要做得细

不只是限制总请求量,还要按接口分组。比如查询接口可以限流 100 次/分钟,但写入接口只能 10 次/分钟。防止有人疯狂调用写入接口把你的数据库打爆。

2. 敏感数据要脱敏

日志里打满手机号、身份证号的接口我见过不少。存储要加密,传输要 HTTPS,返回给前端之前要脱敏。手机号中间四位打星号,身份证号只显示前三位后四位,这是基本操作。

3. CORS 别配置成 *

开发阶段图省事 CORS 设成 *,上线也不改。这种我只能说胆子大。被恶意网站用你的 API 发起 CSRF 攻击,用户数据泄露了哭都来不及。

总结

API 设计这件事,说到底是在给自己和调用方省麻烦。一个设计得好的接口,调用方用起来顺手,自己维护起来也轻松。改需求、加字段、排查问题,都能从容应对。

记住几个核心原则:资源导向、状态码语义化、错误响应有价值、分页保平安、版本管理要规范、安全时刻记心间。做到这几点,不敢说接口设计得多优雅,至少不会被人背后骂。

接口是后端的脸面,且写且珍惜。

相关文章

当AI开始整活:最近那些让我笑出声的新玩意儿
从入门到踩坑:我用 OpenClaw 搞钱的日子
还在为部署AI工具秃头?小龙虾帮你一键搞定!
RESTful API 设计指北:我踩过的那些坑和总结的套路
HTTP状态码:你以为你懂了,其实你只懂了一半
消息队列的坑我都替你踩过了:从小白到达人的队列设计血泪史

发布评论