接口设计原则:RESTful API 设计规范

说实话,很多同学做接口设计,上来就抄社区现成的 URL 格式。我个人习惯先想清楚一件事:这个接口到底是给谁用的? 调用方是人还是程序?数据要写还是读?我在项目中见过太多「漂亮但没法用」的 API,千篇一律的 /api/getUser 配上 POST 请求,最后连自己都分不清哪个是查询、哪个是更新。

RESTful 设计的核心,其实就一句话:用 HTTP 本身的能力去表达操作意图。你想想看,HTTP 协议早就给了 GET/POST/PUT/DELETE,我们何必再自己造一套?

4.1 资源 vs 动作:从命令式到声明式

很多人习惯写 URL 的时候带上动词,比如 /getUser、/deleteOrder。我早期也这么干,直到有一次重构,发现前端改了请求方法,后端路由又要改一遍——太累了。

RESTful 的原则是:URL 只描述资源,不描述动作。

非 RESTful(反例)RESTful(推荐)说明
/getUser?id=1GET /users/1查询用户,动词交给 GET
/createUserPOST /users创建资源,用 POST
/updateUserPUT /users/1 或 PATCH /users/1全量/部分更新
/deleteUser?id=1DELETE /users/1删除资源

这表我建议你存下来。我每次做 API 评审都拿它当 checklist,十次有八次能抓到违规。

避坑指南:我曾经遇到一个项目,所有接口都只暴露 /api 一个路径,靠请求体里的 method 字段区分操作。结果对接方写了个自动化工具,硬编码了十几个 method 名称,后来业务需求一变,改代码改到崩溃。记住了,别让 URL 变成函数名。

4.2 名词复数、层级关系、查询参数

资源命名有几个约定俗成的小规则,我总结一下:

  • 一律用名词复数:/users 而不是 /user,/orders 而不是 /order。这样集合和单例的区分更清晰。
  • 层级用斜杠表达:比如 /users/123/orders 表示用户 123 的订单列表。别用 /getUserOrders?userId=123 那种拼参数的方式。
  • 查询参数只用来过滤、排序、分页:GET /users?status=active&page=2&size=20 没问题。但如果是「获取单个用户」,一定要用路径参数 /users/456。

个人习惯:我一般在设计阶段先画一张资源关系图,把每个实体、它们之间的关联梳理清楚,再转成 URL 结构。这样后期几乎不用改路径。

4.3 HTTP 方法 & 状态码:别只返回 200

很多同学后端不管成不成功,统一返回 200,然后在响应体里写 { code: 1, msg: "success" }。我懂你为什么这么干——方便前端统一处理。但你想过没有,这其实把 HTTP 状态码变成了无意义的摆设。

RESTful 设计的另一个要点是:让 HTTP 状态码替你说话。

场景HTTP 方法成功状态码常见错误码
获取资源列表GET200 OK400(参数错)、404(资源不存在)
创建资源POST201 Created409(冲突,如重复)、422(校验失败)
全量更新PUT200 OK404、409、422
部分更新PATCH200 OK404、422
删除资源DELETE204 No Content404
幂等性校验任何方法参见上表429(频率限制)、401/403(鉴权)

注意:我曾经踩过一个坑—— DELETE 返回 200 并且 body 里还带着被删除的资源。结果调用方以为执行的是「查询」操作,后面又拿这个「返回的资源」去做逻辑,导致数据不一致。所以 DELETE 成功后返回 204,啥也别说。

4.4 安全与幂等:接口契约的生命线

写国密算法接口,安全是底线。但 RESTful 设计里还有一个容易忽略的概念:幂等。

  • GET、PUT、DELETE 必须是幂等的:同一个请求执行多次,结果是一样的。
  • POST 不是幂等的:每次调用都会创建新资源。所以用户在填写表单时有重复提交风险,我一般配合 Idempotency-Key 请求头来处理。

我给你写一个简单的示范(Python + Flask):

from flask import Flask, request, jsonify
import hashlib

app = Flask(__name__)
idempotency_store = {}  # 生产环境用 Redis

@app.before_request
def check_idempotency():
    if request.method == 'POST':
        key = request.headers.get('Idempotency-Key')
        if key and key in idempotency_store:
            return jsonify(idempotency_store[key]), 200
        # 正常处理,完成后 store 里写入响应

这个逻辑我直接用在国密密钥分发接口上,客户端重试时就不会重复创建密钥对。

4.5 版本管理 & 内容协商

接口升级是迟早的事。我建议优先用 URL 前缀版本号:/v1/users、/v2/users。虽然有人认为应该用请求头 Accept: application/vnd.xxx.v2+json,但实际项目中,URL 形式更直观。尤其对接方不一定是资深工程师,一眼能看出版本。

内容协商(Content Negotiation)也要考虑:国密算法接口经常涉及密钥二进制、证书 PEM 等非 JSON 格式。我会在 Accept 头支持 application/json、application/x-pem-file 等类型,然后在 Content-Type 里声明返回格式。

4.6 本章知识体系:一张图说清楚

我把上面讲的核心概念画成结构图,方便你以后查阅:

RESTful 接口设计核心知识体系 RESTful API 设计原则 资源 vs 动作 URL 只描述资源 HTTP 方法 & 状态码 用协议自带语义 安全与幂等 Idempotency-Key 版本管理 /v1/ /v2/ 内容协商 Accept / Content-Type 核心:用 HTTP 协议本身表达意图,URL 只描述资源,不描述动作

4.7 快速自查清单

每次写完接口,我都会对着这个清单扫一遍:

  1. URL 是不是只含名词,没有动词?
  2. 集合资源用复数,单例用 id 路径?
  3. GET 安全且幂等?POST 不幂等?PUT/DELETE 幂等?
  4. 200、201、204、400、404、409 有没有用对?
  5. 版本号是否在路径里?
  6. 返回格式有没有声明 Content-Type?
  7. 国密敏感接口是否加了幂等校验和鉴权?

每一条都是教训换来的。嗯,这节课的内容就到这,把这些原则刻进直觉里,设计接口会轻松很多。