// ❌ 全部用 POST
POST /api/getUser // 获取用户
POST /api/createUser // 创建用户
POST /api/updateUser // 更新用户
POST /api/deleteUser // 删除用户
POST /api/searchUsers // 搜索用户
为什么这样不好?
违反 HTTP 语义:GET 请求应该是幂等的、可缓存的。
无法利用浏览器缓存:POST 请求不会被缓存。
无法使用 CDN:CDN 通常只缓存 GET 请求。
难以调试:浏览器历史记录、书签都无法保存 POST 请求。
正确姿势:RESTful 风格
// ✅ 正确使用 HTTP 方法
GET /api/users // 获取用户列表
GET /api/users/123 // 获取单个用户
POST /api/users // 创建用户
PUT /api/users/123 // 完整更新用户
PATCH /api/users/123 // 部分更新用户
DELETE /api/users/123// 删除用户
HTTP 方法速查表:
方法
用途
幂等性
安全性
请求体
响应体
GET
获取资源
✅
✅
❌
✅
POST
创建资源
❌
❌
✅
✅
PUT
完整更新
✅
❌
✅
✅
PATCH
部分更新
❌
❌
✅
✅
DELETE
删除资源
✅
❌
❌
✅/❌
幂等性:多次执行结果相同
安全性:不会修改服务器状态
实战代码
# Python/Flask 示例from flask import Flask, request, jsonify
app = Flask(__name__)
# 获取用户列表@app.route('/api/users', methods=['GET'])defget_users():
page = request.args.get('page', 1, type=int)
limit = request.args.get('limit', 20, type=int)
users = User.query.paginate(page=page, per_page=limit)
return jsonify([user.to_dict() for user in users.items])
# 获取单个用户@app.route('/api/users/<int:user_id>', methods=['GET'])defget_user(user_id):
user = User.query.get_or_404(user_id)
return jsonify(user.to_dict())
# 创建用户@app.route('/api/users', methods=['POST'])defcreate_user():
data = request.get_json()
user = User(username=data['username'], email=data['email'])
db.session.add(user)
db.session.commit()
return jsonify(user.to_dict()), 201# 完整更新用户@app.route('/api/users/<int:user_id>', methods=['PUT'])defupdate_user(user_id):
user = User.query.get_or_404(user_id)
data = request.get_json()
user.username = data['username']
user.email = data['email']
db.session.commit()
return jsonify(user.to_dict())
# 部分更新用户@app.route('/api/users/<int:user_id>', methods=['PATCH'])defpatch_user(user_id):
user = User.query.get_or_404(user_id)
data = request.get_json()
if'username'in data:
user.username = data['username']
if'email'in data:
user.email = data['email']
db.session.commit()
return jsonify(user.to_dict())
# 删除用户@app.route('/api/users/<int:user_id>', methods=['DELETE'])defdelete_user(user_id):
user = User.query.get_or_404(user_id)
db.session.delete(user)
db.session.commit()
return'', 204# No Content
特殊场景:非 CRUD 操作怎么办?
有些业务操作不是简单的增删改查,比如发送邮件、重置密码、取消订单、点赞文章。
方案 1:把操作建模成资源
// ❌ 不好:用动词
POST /api/sendEmail
POST /api/resetPassword
POST /api/cancelOrder
// ✅ 好:把操作建模成资源
POST /api/emails // 创建一封邮件(发送)
POST /api/password-resets // 创建一个密码重置请求
POST /api/orders/123/cancellation // 创建一个取消订单的操作
方案 2:使用子资源
// 点赞文章
POST /api/posts/123/likes // 点赞
DELETE /api/posts/123/likes // 取消点赞
// 关注用户
POST /api/users/123/followers // 关注
DELETE /api/users/123/followers // 取消关注
方案 3:使用动作端点(最后的选择)
// 如果实在无法建模成资源,可以使用动作端点
POST /api/users/123/actions/activate // 激活用户
POST /api/orders/123/actions/refund // 退款
POST /api/posts/123/actions/publish // 发布文章
// ✅ 好的错误处理// 1. 使用正确的 HTTP 状态码// 400 Bad Request - 客户端错误{"error":{"code":"INVALID_INPUT","message":"Validation failed","details":[{"field":"email","message":"Email format is invalid"},{"field":"age","message":"Age must be greater than 0"}]},"requestId":"req_abc123","timestamp":"2025-01-17T10:30:00Z"}// 401 Unauthorized - 未认证{"error":{"code":"UNAUTHORIZED","message":"Authentication required","details":"Please provide a valid access token"}}// 403 Forbidden - 无权限{"error":{"code":"FORBIDDEN","message":"Insufficient permissions","details":"You need 'admin' role to perform this action"}}// 404 Not Found - 资源不存在{"error":{"code":"RESOURCE_NOT_FOUND","message":"User not found","details":"User with ID 123 does not exist"}}// 429 Too Many Requests - 限流{"error":{"code":"RATE_LIMIT_EXCEEDED","message":"Too many requests","details":"Rate limit: 100 requests per minute","retryAfter":45}}// 500 Internal Server Error - 服务器错误{"error":{"code":"INTERNAL_SERVER_ERROR","message":"An unexpected error occurred","details":"Please contact support if the problem persists","requestId":"req_abc123"}}
HTTP 状态码速查表
2xx 成功
200 OK:请求成功
201 Created:资源创建成功
202 Accepted:请求已接受,但处理未完成(异步)
204 No Content:成功,但无返回内容(常用于 DELETE)
206 Partial Content:部分内容(分页、断点续传)
4xx 客户端错误
400 Bad Request:请求参数错误
401 Unauthorized:未认证(需要登录)
403 Forbidden:无权限
404 Not Found:资源不存在
405 Method Not Allowed:HTTP 方法不支持
409 Conflict:资源冲突(如用户名已存在)
422 Unprocessable Entity:语义错误(如验证失败)
429 Too Many Requests:请求过多(限流)
5xx 服务器错误
500 Internal Server Error:服务器内部错误
502 Bad Gateway:网关错误
503 Service Unavailable:服务不可用(维护中)
504 Gateway Timeout:网关超时
实战代码:统一错误处理
# Python/Flask 示例from flask import Flask, jsonify
from datetime import datetime
import uuid
app = Flask(__name__)
# 自定义异常类classAPIError(Exception):
def__init__(self, code, message, details=None, status_code=400):
self.code = code
self.message = message
self.details = details
self.status_code = status_code
# 全局错误处理器@app.errorhandler(APIError)defhandle_api_error(error):
response = {
'error': {
'code': error.code,
'message': error.message,
'details': error.details
},
'requestId': str(uuid.uuid4()),
'timestamp': datetime.utcnow().isoformat() + 'Z'
}
return jsonify(response), error.status_code
@app.errorhandler(404)defhandle_not_found(error):
return jsonify({'error': {'code': 'RESOURCE_NOT_FOUND', 'message': 'The requested resource was not found'}}), 404@app.errorhandler(500)defhandle_internal_error(error):
app.logger.error(f'Internal error: {error}')
return jsonify({
'error': {
'code': 'INTERNAL_SERVER_ERROR',
'message': 'An unexpected error occurred',
'details': 'Please contact support if the problem persists'
},
'requestId': str(uuid.uuid4())
}), 500# 使用示例@app.route('/api/users/<int:user_id>', methods=['GET'])defget_user(user_id):
user = User.query.get(user_id)
ifnot user:
raise APIError(
code='USER_NOT_FOUND',
message='User not found',
details=f'User with ID {user_id} does not exist',
status_code=404
)
return jsonify(user.to_dict())
// ❌ 各种混乱的 URL 设计
// 问题 1:动词 + 名词混用
GET /api/getUsers
GET /api/user/list
GET /api/fetchUserData
// 问题 2:过度嵌套
GET /api/v1/company/123/department/456/team/789/user/111/posts/222/comments/333
// 问题 3:命名不一致
GET /api/users // 复数
GET /api/product // 单数
GET /api/order-list // 短横线
GET /api/userProfile // 驼峰
// 问题 4:查询参数放在路径里
GET /api/users/search/name/zhangsan/age/25
正确姿势:清晰的 URL 设计
// ✅ 好的 URL 设计原则
// 1. 使用名词,不用动词
GET /api/users // ✅
GET /api/getUsers // ❌
// 2. 使用复数形式
GET /api/users // ✅
GET /api/user // ❌
// 3. 使用短横线分隔单词
GET /api/user-profiles // ✅
GET /api/userProfiles // ❌
GET /api/user_profiles // ❌
// 4. 资源嵌套不超过 2 层
GET /api/users/123/posts // ✅ 获取用户的文章
GET /api/posts?userId=123 // ✅ 也可以用查询参数
GET /api/users/123/posts/456/comments // ❌ 太深了
// 5. 查询、过滤、排序用查询参数
GET /api/users?name=zhangsan&age=25 // ✅
GET /api/users/search/name/zhangsan // ❌
// 6. 分页用查询参数
GET /api/users?page=1&limit=20 // ✅
GET /api/users/page/1/limit/20 // ❌
URL 设计速查表
场景
推荐方式
说明
获取列表
GET /api/users
使用复数名词
获取单个
GET /api/users/123
ID 放在路径中
搜索过滤
GET /api/users?name=zhang&age=25
使用查询参数
分页
GET /api/users?page=1&limit=20
使用查询参数
排序
GET /api/users?sort=createdAt&order=desc
使用查询参数
关联资源
GET /api/users/123/posts
嵌套不超过 2 层
字段筛选
GET /api/users?fields=id,name,email
使用查询参数
版本控制
GET /api/v1/users
版本号放在路径开头
实战示例:完整的用户 API
// 用户管理
GET /api/v1/users // 获取用户列表
GET /api/v1/users/123 // 获取单个用户
POST /api/v1/users // 创建用户
PUT /api/v1/users/123 // 更新用户
PATCH /api/v1/users/123// 部分更新用户
DELETE /api/v1/users/123// 删除用户
// 用户的文章
GET /api/v1/users/123/posts // 获取用户的文章列表
POST /api/v1/users/123/posts// 为用户创建文章
// 用户的关注者
GET /api/v1/users/123/followers // 获取关注者列表
POST /api/v1/users/123/followers // 关注用户
DELETE /api/v1/users/123/followers/456 // 取消关注
// 搜索和过滤
GET /api/v1/users?name=zhang // 按名字搜索
GET /api/v1/users?age=25&city=beijing // 多条件过滤
GET /api/v1/users?status=active // 按状态过滤
// 分页和排序
GET /api/v1/users?page=1&limit=20 // 分页
GET /api/v1/users?sort=createdAt&order=desc // 排序
// 字段筛选(减少响应体积)
GET /api/v1/users?fields=id,name,email // 只返回指定字段
错误 5:没有版本控制,改接口全靠吼
灾难现场
// 第一版 API
GET /api/users/123
{
"name": "张三",
"age": 25
}
// 三个月后,需求变了,直接改接口
GET /api/users/123
{
"firstName": "三",
"lastName": "张",
"birthDate": "1999-01-01"
}
// 结果:所有老版本的 App 全部崩溃
为什么需要版本控制?
向后兼容:老版本的客户端不会因为 API 更新而崩溃。
平滑迁移:给客户端足够的时间升级。
A/B 测试:可以同时运行多个版本。
回滚方便:出问题可以快速回退。
正确姿势:API 版本控制
方案 1:URL 路径版本(推荐)
GET /api/v1/users/123
GET /api/v2/users/123
优点:
清晰明了,一眼就能看出版本。
容易在路由层面做版本隔离。
方便缓存和 CDN。
缺点:
URL 会变化。
方案 2:请求头版本
GET /api/users/123
Accept: application/vnd.myapi.v1+json
GET /api/users/123
Accept: application/vnd.myapi.v2+json
优点:
URL 保持不变。
符合 HTTP 标准。
缺点:
不够直观。
难以在浏览器中测试。
方案 3:查询参数版本
GET /api/users/123?version=1
GET /api/users/123?version=2
// 在响应体中也可以包含废弃信息{"data":{...},"meta":{"deprecated":true,"deprecationDate":"2025-12-31","sunsetDate":"2026-03-31","message":"This API version will be sunset on 2026-03-31. Please migrate to v2.","migrationGuide":"https://docs.example.com/api/v1-to-v2-migration"}}
// 只返回需要的字段
GET /api/users?fields=id,username,email
// 支持嵌套字段
GET /api/users?fields=id,username,profile.avatar,profile.bio
方案 2:不同场景返回不同数据
// 列表视图:只返回摘要信息
GET /api/users
// 详情视图:返回完整信息
GET /api/users/1
方案 3:使用 GraphQL(终极方案)
# 前端精确指定需要的字段query{ users { id username email }}# 需要更多信息时query{ users { id username email profile { avatar bio } posts(limit:5){ id title }}}
实战代码:字段筛选实现
# Python/Flask 示例from flask import Flask, request, jsonify
app = Flask(__name__)
deffilter_fields(data, fields):
ifnot fields:
return data
field_list = fields.split(',')
ifisinstance(data, list):
return [filter_fields(item, fields) for item in data]
ifisinstance(data, dict):
filtered = {}
for field in field_list:
if'.'in field:
parts = field.split('.', 1)
parent, child = parts[0], parts[1]
if parent in data:
if parent notin filtered:
filtered[parent] = {}
filtered[parent].update(filter_fields(data[parent], child))
elif field in data:
filtered[field] = data[field]
return filtered
return data
@app.route('/api/users')defget_users():
users = User.query.all()
data = [user.to_dict() for user in users]
fields = request.args.get('fields')
if fields:
data = filter_fields(data, fields)
return jsonify(data)
// ❌ 每个接口的分页参数都不一样
// 接口 A
GET /api/users?page=1&pageSize=20
// 接口 B
GET /api/posts?pageNum=1&limit=20
// 接口 C
GET /api/comments?p=1&size=20&offset=0
// 接口 D
GET /api/orders?start=0&count=20
为什么需要统一?
降低学习成本:前端不用记住每个接口的参数名。
便于封装:可以写一个通用的分页组件。
减少错误:统一的规范减少参数错误。
提升体验:一致性让 API 更专业。
正确姿势:统一的查询规范
// ✅ 统一的分页、排序、过滤规范
// 分页参数
GET /api/users?page=1&limit=20
// 排序参数
GET /api/users?sort=createdAt&order=desc
GET /api/users?sort=-createdAt // 也可以用负号表示降序
// 过滤参数
GET /api/users?status=active&role=admin
GET /api/users?age[gte]=18&age[lte]=60 // 范围查询
// 搜索参数
GET /api/users?search=zhang
// 组合使用
GET /api/users?page=1&limit=20&sort=-createdAt&status=active&search=zhang
// 等于
GET /api/users?status=active
// 不等于
GET /api/users?status[ne]=inactive
// 大于/小于
GET /api/users?age[gt]=18&age[lt]=60
// 大于等于/小于等于
GET /api/users?age[gte]=18&age[lte]=60
// 包含(数组)
GET /api/users?role[in]=admin,editor
// 不包含
GET /api/users?role[nin]=guest
// 模糊搜索
GET /api/users?username[like]=zhang
// 日期范围
GET /api/users?createdAt[gte]=2024-01-01&createdAt[lte]=2024-12-31
// 多个条件组合
GET /api/users?status=active&age[gte]=18&role[in]=admin,editor&sort=-createdAt