网页服务面向浏览器,API 面向程序。程序之间通信必须约定明确:URL 代表什么、用什么方法、返回什么结构、出错怎么表达——否则前后端联调就是灾难。
直觉类比:REST 就像"餐厅的菜单 + 服务员"——**资源(菜单上的菜)**用名词表示,**动作(点单/退单/换菜)**用标准动词(HTTP 方法)表示,一切围绕资源操作,而不是"执行某个函数"。
💡 关键直觉:REST 的精髓是 URL 只描述资源(名词),不描述动作(动词)。动作交给 HTTP 方法表达。
GET /users/1(读)与DELETE /users/1(删)共用同一 URL,靠方法区分。
| 错误(动词化) | 正确(名词化) |
|---|---|
| GET /getUser | GET /users/1 |
| POST /createUser | POST /users |
| POST /deleteUser | DELETE /users/1 |
| GET /updateUserStatus | PATCH /users/1 |
| 方法 | 语义 | 幂等 |
|---|---|---|
| GET | 读取资源 | 是 |
| POST | 创建资源(或复杂操作) | 否 |
| PUT | 全量更新 | 是 |
| PATCH | 部分更新 | 否 |
| DELETE | 删除资源 | 是 |
幂等:同样请求执行多次,结果一致。GET/PUT/DELETE 幂等,POST 不幂等(每次创建新资源)。
| 状态码 | 含义 | 场景 |
|---|---|---|
| 200 | 成功 | GET/PUT/PATCH 成功 |
| 201 | 已创建 | POST 成功 |
| 204 | 无内容 | DELETE 成功 |
| 400 | 参数错误 | 校验失败 |
| 401 | 未认证 | 没登录 |
| 403 | 无权限 | 登录但没权限 |
| 404 | 资源不存在 | URL 错了或记录没了 |
| 409 | 冲突 | 邮箱重复等 |
| 422 | 无法处理 | 语义错误(如缺字段) |
| 500 | 服务器错误 | 未捕获异常 |
前后端约定统一的错误 JSON:
{ "error": { "code": "USER_NOT_FOUND", "message": "用户 42 不存在", "details": {} } }
URL 前缀带版本:/api/v1/users、/api/v2/users。破坏性变更时升版本,老客户端不受影响。
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
列表接口必须分页,避免一次返回海量数据:
@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, } })
多资源时用蓝图分组:
from flask import Blueprint users_api = Blueprint('users_api', __name__, url_prefix='/api/v1/users') # 把上述路由挂到 users_api 上 app.register_blueprint(users_api)
手写校验在小项目够用;复杂项目用 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

| 场景 | 推荐 | 原因 |
|---|---|---|
| 简单 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 -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
状态码 + 错误结构配合,前端可以精确处理每一种情况——这就是契约的价值。
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 都可以自动从代码生成文档。**"接口即文档"**让前后端协作从"反复对口径"变成"查文档即懂"。