第3章:RESTful API 设计原则:资源、URI、CRUD 操作
好,我们直接切入正题。API 接口设计,说白了,就是定义一套你跟前端、跟第三方系统沟通的“暗号”。这套暗号如果设计得乱七八糟,后期维护起来,你想想看,得有多痛苦?
RESTful 不是什么高深莫测的玄学,它是一套很朴素的约定。核心就三个东西:资源(Resource)、URI、以及 CRUD 操作。把这三样理清了,接口设计就成功了一大半。
3.1 什么是“资源”?—— 你得先想清楚你要操作什么
我个人习惯,在设计接口之前,先不做技术实现,而是画一张业务领域的“实体图”。比如自动录入系统,它的核心资源是什么?说白了,就是任务(Task)、模板(Template)、记录(Record)。这些就是我们需要暴露给外界的“资源”。
资源是对业务数据的抽象。它不应该是一个数据库表,而是一个逻辑上的概念。例如,一个“导入任务”可能涉及多个表的数据,但在 API 层面,它就是一个完整的资源。
设计资源时,有个原则要记住:用名词,而不是动词。因为名词代表了实体,动词代表了动作。动作应该由 HTTP 方法(GET、POST 等)来表达。
3.2 URI 是什么?—— 资源的门牌号
URI 就是资源的地址。好的 URI 设计,应该是自解释的。看到地址,你就大概知道这个端点是干什么的。
我们来看看一个典型的设计案例。对于“自动录入任务”这个资源,它的 URI 应该怎么设计?
| 动作意图 | 不推荐的 URI | 推荐的 URI | 说明 |
|---|---|---|---|
| 获取所有任务 | /getTasks |
/api/v1/tasks |
用名词复数,动词交给 HTTP 方法 |
| 获取单个任务 | /getTaskById?id=5 |
/api/v1/tasks/5 |
路径参数传递 ID,语义清晰 |
| 创建新任务 | /createTask |
/api/v1/tasks |
POST 请求,向集合追加资源 |
| 更新任务状态 | /updateStatus |
/api/v1/tasks/5/status |
子资源表达“资源的属性” |
设计 URI 时,有几个铁律要注意:
- 统一前缀:比如
/api/v1/。前缀用于区分版本或服务类型,方便后续升级。 - 层级关系:用斜杠
/表达资源之间的从属关系,但层级不要太深,3-4 层就够了。比如/tasks/5/records表示“某个任务下的所有记录”。 - 单数还是复数? 业内大部分实践倾向使用复数名词,如
/tasks、/templates。保持一致即可。 - 查询参数:用于过滤、排序、分页,这些不是资源标识符,而是修饰符。例如
/api/v1/tasks?status=success。
3.3 CRUD 操作 —— 用 HTTP 方法说话
现在我们有了资源(Task)和门牌号(/api/v1/tasks)。那怎么对它进行“增删改查”呢?这就要用到 HTTP 请求方法了。
最常见的四种操作,对应数据库里的 CRUD:
| 操作 | HTTP 方法 | URI 示例 | 语义 |
|---|---|---|---|
| 查询(Read) | GET | /api/v1/tasks | 获取资源列表,幂等(多次请求不改变后端数据) |
| 新增(Create) | POST | /api/v1/tasks | 创建新资源,不幂等(每次请求都可能创建新资源) |
| 替换(Update) | PUT | /api/v1/tasks/5 | 整体替换资源,幂等。请求体必须包含所有字段 |
| 部分更新(Partial) | PATCH | /api/v1/tasks/5 | 只更新资源的部分字段,通常用于修改状态 |
| 删除(Delete) | DELETE | /api/v1/tasks/5 | 删除资源,幂等。通常返回 204 No Content |
核心要点: 不要把所有的操作都堆在 GET 和 POST 上。我曾见过一个遗留系统,硬生生用 GET 实现了删除功能,结果爬虫一个脚本跑下来,整个测试库被清空了。把语义和方法绑定好,这是安全性的基本功。
3.4 实战中的设计规范
纸上谈兵没意思,我们落地到实战。设计一个“自动录入系统”的接口,我们通常会定义这样几个端点:
GET /api/v1/tasks:查询所有任务列表,支持分页和状态过滤。POST /api/v1/tasks:创建一个新的录入任务,请求体包含模板 ID、数据文件地址等。GET /api/v1/tasks/{taskId}:获取特定任务的详细信息,包括执行进度、结果统计。PATCH /api/v1/tasks/{taskId}/status:修改任务状态,常见操作如“暂停”、“终止”。DELETE /api/v1/tasks/{taskId}:删除一个任务,如果任务正在执行,通常要先终止。
状态码也很重要。 不要所有成功都返回 200,所有失败都返回 500。要体现语义:
200 OK:查询、更新成功。201 Created:资源创建成功。204 No Content:删除成功,或者操作返回为空。400 Bad Request:前端参数错误,比如缺少必填字段。404 Not Found:资源不存在。409 Conflict:资源冲突,比如重复提交。
3.5 入门案例:用代码落实 URI 与 CRUD
下面是一个用 Python Flask 框架写的小示例,演示如何为一个简单的 Task 资源实现 RESTful 接口:
from flask import Flask, jsonify, request, make_response
app = Flask(__name__)
# 模拟的内存数据库
tasks_db = [
{'id': 1, 'title': '导入销售数据', 'status': 'pending'},
{'id': 2, 'title': '同步客户表', 'status': 'success'}
]
@app.route('/api/v1/tasks', methods=['GET'])
def get_tasks():
return jsonify(tasks_db)
@app.route('/api/v1/tasks/<int:task_id>', methods=['GET'])
def get_task(task_id):
task = next((t for t in tasks_db if t['id'] == task_id), None)
if task:
return jsonify(task)
return make_response(jsonify({'error': 'Task not found'}), 404)
@app.route('/api/v1/tasks', methods=['POST'])
def create_task():
if not request.json or 'title' not in request.json:
return make_response(jsonify({'error': 'Bad request'}), 400)
new_task = {
'id': tasks_db[-1]['id'] + 1 if tasks_db else 1,
'title': request.json['title'],
'status': 'pending'
}
tasks_db.append(new_task)
return jsonify(new_task), 201
@app.route('/api/v1/tasks/<int:task_id>', methods=['DELETE'])
def delete_task(task_id):
global tasks_db
task = next((t for t in tasks_db if t['id'] == task_id), None)
if task:
tasks_db = [t for t in tasks_db if t['id'] != task_id]
return '', 204
return make_response(jsonify({'error': 'Not found'}), 404)
if __name__ == '__main__':
app.run(debug=True)
这个例子虽然简单,但麻雀虽小五脏俱全。它严格遵守了名词 + 方法的约定,并且返回了正确的状态码。嗯,这里要注意一点:实际生产环境中,你千万别把数据全存在内存里,这里只是为了演示结构。
/api/do,通过请求体里的 action 字段来区分操作。比如 action: getTaskList,action: createTask。维护了半年后,代码耦合严重,每个接口函数都有上千行。这就是典型的“反 RESTful”设计,切忌。3.6 本章知识结构图
最后,我们用一张图来总结一下 RESTful API 设计的核心三要素以及它们之间的关系: