接口设计原则: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=1 | GET /users/1 | 查询用户,动词交给 GET |
/createUser | POST /users | 创建资源,用 POST |
/updateUser | PUT /users/1 或 PATCH /users/1 | 全量/部分更新 |
/deleteUser?id=1 | DELETE /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 方法 | 成功状态码 | 常见错误码 |
|---|---|---|---|
| 获取资源列表 | GET | 200 OK | 400(参数错)、404(资源不存在) |
| 创建资源 | POST | 201 Created | 409(冲突,如重复)、422(校验失败) |
| 全量更新 | PUT | 200 OK | 404、409、422 |
| 部分更新 | PATCH | 200 OK | 404、422 |
| 删除资源 | DELETE | 204 No Content | 404 |
| 幂等性校验 | 任何方法 | 参见上表 | 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 本章知识体系:一张图说清楚
我把上面讲的核心概念画成结构图,方便你以后查阅:
4.7 快速自查清单
每次写完接口,我都会对着这个清单扫一遍:
- URL 是不是只含名词,没有动词?
- 集合资源用复数,单例用 id 路径?
- GET 安全且幂等?POST 不幂等?PUT/DELETE 幂等?
- 200、201、204、400、404、409 有没有用对?
- 版本号是否在路径里?
- 返回格式有没有声明 Content-Type?
- 国密敏感接口是否加了幂等校验和鉴权?
每一条都是教训换来的。嗯,这节课的内容就到这,把这些原则刻进直觉里,设计接口会轻松很多。