2.8 RESTful API 设计


2.8 RESTful API 设计

问题与直觉:API 是"接口协议"

网页服务面向浏览器,API 面向程序。程序之间通信必须约定明确:URL 代表什么、用什么方法、返回什么结构、出错怎么表达——否则前后端联调就是灾难。

直觉类比:REST 就像"餐厅的菜单 + 服务员"——**资源(菜单上的菜)**用名词表示,**动作(点单/退单/换菜)**用标准动词(HTTP 方法)表示,一切围绕资源操作,而不是"执行某个函数"。

💡 关键直觉:REST 的精髓是 URL 只描述资源(名词),不描述动作(动词)。动作交给 HTTP 方法表达。GET /users/1(读)与 DELETE /users/1(删)共用同一 URL,靠方法区分。

核心原理:REST 的五个约定

2.1 资源用名词

错误(动词化) 正确(名词化)
GET /getUser GET /users/1
POST /createUser POST /users
POST /deleteUser DELETE /users/1
GET /updateUserStatus PATCH /users/1

2.2 HTTP 方法 = 动作语义

方法 语义 幂等
GET 读取资源
POST 创建资源(或复杂操作)
PUT 全量更新
PATCH 部分更新
DELETE 删除资源

幂等:同样请求执行多次,结果一致。GET/PUT/DELETE 幂等,POST 不幂等(每次创建新资源)。

2.3 状态码表达结果

状态码 含义 场景
200 成功 GET/PUT/PATCH 成功
201 已创建 POST 成功
204 无内容 DELETE 成功
400 参数错误 校验失败
401 未认证 没登录
403 无权限 登录但没权限
404 资源不存在 URL 错了或记录没了
409 冲突 邮箱重复等
422 无法处理 语义错误(如缺字段)
500 服务器错误 未捕获异常

2.4 统一错误结构

前后端约定统一的错误 JSON:

{ "error": { "code": "USER_NOT_FOUND", "message": "用户 42 不存在", "details": {} } }

2.5 版本化

URL 前缀带版本:/api/v1/users/api/v2/users。破坏性变更时升版本,老客户端不受影响。

工程实践要点:用 Flask 实现

3.1 基础 CRUD 实现

from flask import Flask, request, jsonify, abort from flask_sqlalchemy import SQLAlchemy app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db' db = SQLAlchemy(app) class User(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(80), nullable=False) email = db.Column(db.String(120), unique=True, nullable=False) # 列表 @app.route('/api/users', methods=['GET']) def list_users(): users = User.query.all() return jsonify([{'id': u.id, 'name': u.name, 'email': u.email} for u in users]) # 创建 @app.route('/api/users', methods=['POST']) def create_user(): data = request.get_json() or {} if not data.get('name') or not data.get('email'): return jsonify({'error': {'code': 'MISSING_FIELD', 'message': 'name 与 email 必填'}}), 400 if User.query.filter_by(email=data['email']).first(): return jsonify({'error': {'code': 'EMAIL_EXISTS', 'message': '邮箱已存在'}}), 409 user = User(name=data['name'], email=data['email']) db.session.add(user) db.session.commit() return jsonify({'id': user.id, 'name': user.name, 'email': user.email}), 201 # 详情 @app.route('/api/users/<int:uid>', methods=['GET']) def get_user(uid): user = db.session.get(User, uid) if user is None: return jsonify({'error': {'code': 'USER_NOT_FOUND', 'message': f'用户 {uid} 不存在'}}), 404 return jsonify({'id': user.id, 'name': user.name, 'email': user.email}) # 更新 @app.route('/api/users/<int:uid>', methods=['PUT']) def update_user(uid): user = db.session.get(User, uid) if user is None: return jsonify({'error': {'code': 'USER_NOT_FOUND', 'message': f'用户 {uid} 不存在'}}), 404 data = request.get_json() or {} user.name = data.get('name', user.name) user.email = data.get('email', user.email) db.session.commit() return jsonify({'id': user.id, 'name': user.name, 'email': user.email}) # 删除 @app.route('/api/users/<int:uid>', methods=['DELETE']) def delete_user(uid): user = db.session.get(User, uid) if user is None: return jsonify({'error': {'code': 'USER_NOT_FOUND', 'message': f'用户 {uid} 不存在'}}), 404 db.session.delete(user) db.session.commit() return '', 204

3.2 分页

列表接口必须分页,避免一次返回海量数据:

@app.route('/api/users', methods=['GET']) def list_users(): page = request.args.get('page', 1, type=int) per_page = min(request.args.get('per_page', 20, type=int), 100) pagination = User.query.paginate(page=page, per_page=per_page) return jsonify({ 'items': [{'id': u.id, 'name': u.name, 'email': u.email} for u in pagination.items], 'meta': { 'page': page, 'per_page': per_page, 'total': pagination.total, 'pages': pagination.pages, } })

3.3 蓝图组织 API

多资源时用蓝图分组:

from flask import Blueprint users_api = Blueprint('users_api', __name__, url_prefix='/api/v1/users') # 把上述路由挂到 users_api 上 app.register_blueprint(users_api)

3.4 校验:从手写走向扩展

手写校验在小项目够用;复杂项目用 Marshmallow(序列化+校验)或 Flask-RESTful(第三章详述):

from marshmallow import Schema, fields, ValidationError class UserSchema(Schema): name = fields.Str(required=True, validate=lambda s: len(s) >= 2) email = fields.Email(required=True) schema = UserSchema() try: data = schema.load(request.get_json() or {}) except ValidationError as e: return jsonify({'error': {'code': 'VALIDATION_ERROR', 'message': e.messages}}), 400

RESTful API 设计流程

RESTful API 设计流程

REST vs 其他风格:何时选谁

场景 推荐 原因
简单 CRUD、前后端分离 REST 简单、缓存友好、生态成熟
复杂关联查询、前端要灵活取数 GraphQL 客户端自定义查询结构
内部服务、动作型操作多 RPC(gRPC) 高性能、强类型、流式支持
极简内部接口 RPC(JSON-RPC) 实现最快

常见误区与排查

误区 现象 正解
URL 带动词 /api/getUser?id=1 改成 GET /api/users/1
状态码一律 200 前端难判断成败 用 4xx/5xx 表达错误
错误信息只有英文/只有堆栈 联调痛苦 统一 code+message 结构
列表不分页 数据多了响应超时 分页 + 上限保护
版本裸奔 改字段老客户端全挂 /api/v1 前缀
返回整张表所有字段 泄露敏感字段 序列化时白名单字段

动手演练:curl 验证你的 API

# 创建 curl -X POST http://127.0.0.1:5000/api/users \ -H "Content-Type: application/json" \ -d '{"name":"alice","email":"a@example.com"}' # → 201 {"id":1,"name":"alice","email":"a@example.com"} # 重复创建(应 409) curl -X POST http://127.0.0.1:5000/api/users \ -H "Content-Type: application/json" \ -d '{"name":"bob","email":"a@example.com"}' # → 409 {"error":{"code":"EMAIL_EXISTS",...}} # 详情(应 200) curl http://127.0.0.1:5000/api/users/1 # 不存在(应 404) curl http://127.0.0.1:5000/api/users/999 # 删除(应 204) curl -X DELETE http://127.0.0.1:5000/api/users/1 -i

状态码 + 错误结构配合,前端可以精确处理每一种情况——这就是契约的价值。

本章回顾

  • 资源化:URL 用名词描述资源,方法表达动作,同一 URL 靠方法区分读写删。
  • 状态码语义化:200/201/204 成功,4xx 客户端错误,5xx 服务器错误。
  • 统一错误结构:code(机器可读)+ message(人类可读)+ details。
  • 工程细节:分页、版本化、蓝图分组、校验(手写或 Marshmallow)。
  • 风格取舍:CRUD 用 REST;复杂取数用 GraphQL;高性能内部服务用 gRPC。
  • 契约价值:API 设计先行,前后端按约定开发,联调成本大幅降低。

深入理解:API 设计的实战决策

2.8 讲了 REST 的规范,但真实项目中每个决策都有取舍。几个高频实战问题。

第一,列表接口的参数设计。 除了 page/per_page,真实项目还需要:排序(sort=created_at, order=desc)、过滤(status=published、keyword 搜索)、字段裁剪(fields=id,title,减少传输)。参数多了要有文档(OpenAPI/Swagger),否则调用方全靠猜。**一个实用原则**:列表接口的响应必须包含分页元信息(total、page、per_page、has_more),前端才知道"还有没有下一页"。

第二,创建与更新的幂等性。 POST /users 每次调用都会创建一个新资源(不幂等),网络重试可能导致重复创建。解决思路:客户端生成请求 id(Idempotency-Key 头),服务端记录已处理的请求 id,重复请求返回第一次的结果。支付类接口尤其需要幂等设计——"重复提交不产生副作用"是可靠 API 的基础

第三,错误信息的国际化与版本化。 面向多语言用户,error code 用稳定字符串(USER_NOT_FOUND),message 按用户语言翻译;面向长期维护,破坏性变更升版本(/api/v2),旧版本保留一段时间再废弃(deprecation 周期)。版本化不是银弹——频繁升版本同样让调用方疲惫,好的设计是"向后兼容优先,破坏性变更集中发布"。

第四,认证与权限。 API 的认证和网页不同:网页靠 session Cookie,API 常用 Token(客户端每次请求带 Authorization 头)。简单场景用 API Key;标准做法是 JWT(无状态,服务端不存 session)或 OAuth2(第三方授权)。Flask 生态常用 Flask-JWT-Extended 或 Flask-HTTPAuth。注意:API 认证下,2.6 测试里"注入 session"的方式不适用,需要在测试里构造 Token。

第五,限流与配额。 公开 API 必须限流(Rate Limit):每用户每分钟 N 次请求,超出返回 429。Flask-Limiter 是常用扩展,支持按 IP、按用户维度限流。限流保护的是服务端——防止恶意调用拖垮系统,也是 API 商业化的基础(免费配额 vs 付费扩容)。

第六,API 文档与测试一体。用 OpenAPI(Swagger)描述接口,工具自动生成文档页面、生成客户端 SDK、做契约测试。Flask-RESTX(RESTful 的文档增强版)或 apispec 都可以自动从代码生成文档。**"接口即文档"**让前后端协作从"反复对口径"变成"查文档即懂"。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U