程序员生存指南04-为什么AI能写70%的代码,但取代不了你?2026年程序员核心价值转变:不是写代码,而是设计系统-CSDN博客
AI面试高频问题及原理01- 搞不清AI Agent和LLM的区别?3分钟让你彻底明白-CSDN博客
目录
- 开篇:那些年我们踩过的API坑
- 一、RESTful到底是什么?别再用RPC思维了
- 二、URL设计:别再让前端看不懂你的接口
- 三、HTTP方法:GET、POST、PUT、PATCH、DELETE到底怎么用
- 四、状态码:2xx、3xx、4xx、5xx的正确打开方式
- 五、版本管理:URL路径 vs Header,选哪个
- 六、分页、过滤、排序:别让API变成数据黑洞
- 七、错误响应:统一格式,拒绝"黑盒"
- 八、互动挑战:检查你的API是否符合规范
- 九、源码获取与思考题
- 十、系列预告
开篇:那些年我们踩过的API坑
还记得刚入行那会儿,我写了一个 /getUserInfo?id=123 的接口, senior 看了直摇头:“你这API,RESTful 了个寂寞。”
我当时一脸懵:这不就是获取用户信息吗?有什么问题?
后来踩坑多了才明白:RESTful 不是简单的"用URL传参数",而是一套完整的设计哲学。今天这篇文章,我会把这些年踩过的坑、看过的烂代码、以及大厂的最佳实践,一次性分享给你。
读完本文,你将获得:
- 一套可直接落地的 RESTful API 设计规范
- 10个常见错误的避坑指南
- 可直接复用的代码模板
一、RESTful到底是什么?别再用RPC思维了
1.1 RESTful vs RPC:两种思维模式的碰撞
很多开发者(包括当年的我)写 API 时,下意识用的是 RPC 思维:
RPC 风格:
/getUserInfo ← 动作 + 对象
/createOrder ← 动作 + 对象
/updateProduct ← 动作 + 对象
而 RESTful 的核心思想是 “资源导向”:
RESTful 风格:
GET /users/123 ← 获取资源
POST /orders ← 创建资源
PUT /products/456 ← 更新资源
DELETE /comments/789 ← 删除资源
1.2 RESTful 架构图
┌─────────────────────────────────────────────────────────────┐
│ RESTful 架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ HTTP Methods ┌──────────────────┐ │
│ │ │ ─────────────────→ │ │ │
│ │ Client │ GET/POST/PUT │ Server │ │
│ │ │ ←───────────────── │ (Resources) │ │
│ └──────────┘ Status Codes └──────────────────┘ │
│ ↑ │ │
│ │ ↓ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Stateless│ │ Resource │ │
│ │ Cacheable│ │ /users │ │
│ └──────────┘ │ /orders │ │
│ └──────────┘ │
│ │
│ 核心原则: │
│ • 资源是核心(名词) │
│ • HTTP方法定义操作 │
│ • 无状态通信 │
│ • 统一接口 │
└─────────────────────────────────────────────────────────────┘
1.3 RESTful 的六个核心约束
| Client-Server | 客户端-服务器分离 | 前后端解耦,独立演进 |
| Stateless | 无状态 | 每个请求独立,服务端不保存客户端状态 |
| Cacheable | 可缓存 | 响应明确是否可缓存,提升性能 |
| Uniform Interface | 统一接口 | 资源标识、自描述消息、HATEOAS |
| Layered System | 分层系统 | 客户端不知道是否直连服务器 |
| Code on Demand | 按需代码(可选) | 服务端可下发代码(如JS) |
二、URL设计:别再让前端看不懂你的接口
2.1 黄金法则:名词复数,不用动词
❌ 错误示范:
/getUsers
/createOrder
/updateProductInfo
/deleteCommentById
✅ 正确姿势:
GET /users # 获取用户列表
POST /orders # 创建订单
PUT /products/{id} # 更新产品
DELETE /comments/{id} # 删除评论
2.2 层级关系:用URL表达资源关联
/users/{userId}/orders # 某用户的所有订单
/users/{userId}/orders/{orderId} # 某用户的特定订单
/products/{id}/reviews # 某产品的所有评价
层级关系图:
┌─────────────┐
│ API │
│ Root │
└──────┬──────┘
│
┌───────────────┼───────────────┐
↓ ↓ ↓
┌─────────┐ ┌─────────┐ ┌─────────┐
│ /users │ │/products│ │ /orders │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
┌─────┴─────┐ ┌────┴────┐ ┌────┴────┐
↓ ↓ ↓ ↓ ↓ ↓
┌───────┐ ┌───────┐ │ ┌────────┐ │ ┌────────┐
│/users │ │/users │ │ │/orders │ │ │/orders │
│ /123 │ │ /456 │ └────│/{id} │ └────│/{id} │
└───┬───┘ └───────┘ └───┬────┘ └───┬────┘
│ │ │
┌───┴───┐ ┌────┴────┐ ┌────┴────┐
│/orders│ │/reviews │ │/items │
└───────┘ └─────────┘ └─────────┘
2.3 URL设计Checklist
| 使用小写字母 | /users | /Users |
| 使用连字符分隔 | /user-profiles | /user_profiles |
| 不用下划线 | /order-items | /order_items |
| 不用文件扩展名 | /users | /users.json |
| 名词复数 | /orders | /order |
| 无动作动词 | POST /orders | /createOrder |
三、HTTP方法:GET、POST、PUT、PATCH、DELETE到底怎么用
3.1 HTTP方法语义对照表
┌─────────────┬─────────────────┬────────────────────────────────┐
│ Method │ 语义 │ 使用场景 │
├─────────────┼─────────────────┼────────────────────────────────┤
│ GET │ 获取资源 │ 查询、读取,无副作用 │
│ POST │ 创建资源 │ 新建数据,服务器分配ID │
│ PUT │ 全量更新 │ 替换整个资源(提供完整资源) │
│ PATCH │ 局部更新 │ 修改部分字段(提供变更字段) │
│ DELETE │ 删除资源 │ 删除指定资源 │
│ HEAD │ 获取元数据 │ 检查资源是否存在,不返回body │
│ OPTIONS │ 获取支持方法 │ CORS预检请求 │
└─────────────┴─────────────────┴────────────────────────────────┘
3.2 实战代码示例
# Flask 示例
from flask import Flask, request, jsonify
app = Flask(__name__)
# ===== GET:获取资源 =====
@app.route('/users', methods=['GET'])
def get_users():
"""获取用户列表(支持分页、过滤)"""
page = request.args.get('page', 1, type=int)
size = request.args.get('size', 10, type=int)
# … 查询逻辑
return jsonify({
"data": users,
"pagination": {
"page": page,
"size": size,
"total": total
}
})
@app.route('/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
"""获取单个用户"""
user = find_user_by_id(user_id)
if not user:
return jsonify({"error": "User not found"}), 404
return jsonify(user)
# ===== POST:创建资源 =====
@app.route('/users', methods=['POST'])
def create_user():
"""创建新用户"""
data = request.get_json()
# 参数校验
if not data or 'name' not in data:
return jsonify({"error": "Name is required"}), 400
new_user = create_user_in_db(data)
return jsonify(new_user), 201 # 201 Created
# ===== PUT:全量更新 =====
@app.route('/users/<int:user_id>', methods=['PUT'])
def update_user(user_id):
"""全量更新用户信息"""
data = request.get_json()
# PUT 要求提供完整资源
required_fields = ['name', 'email', 'age']
for field in required_fields:
if field not in data:
return jsonify({"error": f"{field} is required for PUT"}), 400
updated_user = replace_user(user_id, data)
return jsonify(updated_user)
# ===== PATCH:局部更新 =====
@app.route('/users/<int:user_id>', methods=['PATCH'])
def patch_user(user_id):
"""局部更新用户信息"""
data = request.get_json()
# PATCH 只更新提供的字段
updated_user = partial_update_user(user_id, data)
return jsonify(updated_user)
# ===== DELETE:删除资源 =====
@app.route('/users/<int:user_id>', methods=['DELETE'])
def delete_user(user_id):
"""删除用户"""
delete_user_by_id(user_id)
return '', 204 # 204 No Content
if __name__ == '__main__':
app.run(debug=True)
3.3 PUT vs PATCH:到底选哪个?
场景:更新用户信息(name, email, age)
PUT /users/123
{
"name": "张三",
"email": "zhangsan@example.com",
"age": 25
}
→ 必须提供所有字段,未提供的字段会被清空或设为默认值
PATCH /users/123
{
"age": 26
}
→ 只更新 age 字段,其他字段保持不变
建议:
- 前端提供完整表单 → 用 PUT
- 前端只修改个别字段 → 用 PATCH
- 不确定时 → 优先用 PATCH(更安全)
四、状态码:2xx、3xx、4xx、5xx的正确打开方式
4.1 状态码速查表
┌─────────┬────────────────────────────────────────────────────────────┐
│ 2xx │ 成功 │
├─────────┼────────────────────────────────────────────────────────────┤
│ 200 │ OK – 请求成功,返回所请求的数据 │
│ 201 │ Created – 资源创建成功(POST请求) │
│ 202 │ Accepted – 请求已接受,异步处理中 │
│ 204 │ No Content – 成功但无返回内容(DELETE常用) │
├─────────┼────────────────────────────────────────────────────────────┤
│ 3xx │ 重定向 │
├─────────┼────────────────────────────────────────────────────────────┤
│ 301 │ Moved Permanently – 永久重定向 │
│ 302 │ Found – 临时重定向 │
│ 304 │ Not Modified – 资源未修改,使用缓存 │
├─────────┼────────────────────────────────────────────────────────────┤
│ 4xx │ 客户端错误 │
├─────────┼────────────────────────────────────────────────────────────┤
│ 400 │ Bad Request – 请求参数错误 │
│ 401 │ Unauthorized – 未认证(需要登录) │
│ 403 │ Forbidden – 无权限(已登录但无权限) │
│ 404 │ Not Found – 资源不存在 │
│ 409 │ Conflict – 资源冲突(如重复创建) │
│ 422 │ Unprocessable Entity – 语义错误(如邮箱格式不对) │
│ 429 │ Too Many Requests – 请求过于频繁 │
├─────────┼────────────────────────────────────────────────────────────┤
│ 5xx │ 服务端错误 │
├─────────┼────────────────────────────────────────────────────────────┤
│ 500 │ Internal Server Error – 服务器内部错误 │
│ 502 │ Bad Gateway – 网关错误 │
│ 503 │ Service Unavailable – 服务不可用(维护中/过载) │
│ 504 │ Gateway Timeout – 网关超时 │
└─────────┴────────────────────────────────────────────────────────────┘
4.2 常见错误:401 vs 403
这是最容易混淆的一对状态码:
┌─────────────────────────────────────────────────────────────────────┐
│ 401 vs 403 区别 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 401 Unauthorized 403 Forbidden │
│ ─────────────── ───────────── │
│ │
│ "你是谁?我不认识你!" "我知道你是谁,但你没权限!" │
│ │
│ • 未提供认证信息 • 已提供认证信息 │
│ • Token 无效/过期 • Token 有效 │
│ • 需要登录 • 权限不足 │
│ │
│ 示例: 示例: │
│ GET /admin/users GET /admin/users │
│ Header: 无/Token无效 Header: Bearer {普通用户Token} │
│ → 401 → 403 │
│ │
└─────────────────────────────────────────────────────────────────────┘
4.3 状态码使用示例
# 400 Bad Request – 参数错误
@app.route('/users', methods=['POST'])
def create_user():
data = request.get_json()
if not data.get('email'):
return jsonify({
"error": "Bad Request",
"message": "Email is required",
"code": "MISSING_FIELD"
}), 400
# 401 Unauthorized – 未认证
@app.route('/profile', methods=['GET'])
def get_profile():
token = request.headers.get('Authorization')
if not token:
return jsonify({
"error": "Unauthorized",
"message": "Authentication required",
"code": "NO_TOKEN"
}), 401
# 403 Forbidden – 无权限
@app.route('/admin/users', methods=['DELETE'])
def delete_user(user_id):
current_user = get_current_user()
if not current_user.is_admin:
return jsonify({
"error": "Forbidden",
"message": "Admin permission required",
"code": "INSUFFICIENT_PERMISSION"
}), 403
# 404 Not Found – 资源不存在
@app.route('/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
user = find_user(user_id)
if not user:
return jsonify({
"error": "Not Found",
"message": f"User {user_id} not found",
"code": "USER_NOT_FOUND"
}), 404
# 409 Conflict – 资源冲突
@app.route('/users', methods=['POST'])
def create_user():
data = request.get_json()
if user_exists(data['email']):
return jsonify({
"error": "Conflict",
"message": "Email already registered",
"code": "EMAIL_EXISTS"
}), 409
# 422 Unprocessable Entity – 语义错误
@app.route('/users', methods=['POST'])
def create_user():
data = request.get_json()
if not is_valid_email(data['email']):
return jsonify({
"error": "Unprocessable Entity",
"message": "Invalid email format",
"code": "INVALID_EMAIL"
}), 422
五、版本管理:URL路径 vs Header,选哪个
5.1 三种版本管理策略对比
┌──────────────────────────────────────────────────────────────────────┐
│ API 版本管理策略对比 │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ 1. URL 路径版本(推荐) │
│ ───────────────────── │
│ /v1/users │
│ /v2/users │
│ │
│ ✅ 优点:直观、易于调试、可直接在浏览器测试 │
│ ❌ 缺点:URL 变更,破坏 REST "资源不变"原则 │
│ │
│ 2. Header 版本 │
│ ──────────────── │
│ /users │
│ Accept: application/vnd.api.v1+json │
│ │
│ ✅ 优点:URL 不变,符合 REST 原则 │
│ ❌ 缺点:调试麻烦,需要工具设置 Header │
│ │
│ 3. 查询参数版本 │
│ ──────────────── │
│ /users?version=1 │
│ │
│ ✅ 优点:简单 │
│ ❌ 缺点:不标准,容易被忽略 │
│ │
└──────────────────────────────────────────────────────────────────────┘
5.2 推荐方案:URL 路径版本
# Flask 蓝图实现版本控制
from flask import Blueprint
# v1 版本
v1_bp = Blueprint('api_v1', __name__, url_prefix='/v1')
@v1_bp.route('/users', methods=['GET'])
def get_users_v1():
"""v1 版本:返回简单用户信息"""
return jsonify({
"users": [
{"id": 1, "name": "张三"}
]
})
# v2 版本
v2_bp = Blueprint('api_v2', __name__, url_prefix='/v2')
@v2_bp.route('/users', methods=['GET'])
def get_users_v2():
"""v2 版本:返回更详细的用户信息"""
return jsonify({
"data": {
"users": [
{
"id": 1,
"name": "张三",
"profile": {…},
"meta": {…}
}
]
},
"pagination": {…}
})
# 注册蓝图
app.register_blueprint(v1_bp)
app.register_blueprint(v2_bp)
六、分页、过滤、排序:别让API变成数据黑洞
6.1 分页设计
GET /users?page=1&size=20
@app.route('/users', methods=['GET'])
def get_users():
# 参数解析
page = request.args.get('page', 1, type=int)
size = request.args.get('size', 20, type=int)
# 限制最大页大小,防止性能问题
size = min(size, 100)
# 查询数据
users, total = query_users(page=page, size=size)
# 计算分页信息
total_pages = (total + size – 1) // size
return jsonify({
"data": users,
"pagination": {
"page": page,
"size": size,
"total": total,
"total_pages": total_pages,
"has_next": page < total_pages,
"has_prev": page > 1
}
})
6.2 过滤设计
GET /users?status=active&role=admin&created_after=2024-01-01
@app.route('/users', methods=['GET'])
def get_users():
# 构建查询条件
filters = {}
if 'status' in request.args:
filters['status'] = request.args.get('status')
if 'role' in request.args:
filters['role'] = request.args.get('role')
if 'created_after' in request.args:
filters['created_at__gte'] = request.args.get('created_after')
users = query_users_with_filters(filters)
return jsonify({"data": users})
6.3 排序设计
GET /users?sort=-created_at,+name
# – 表示降序,+ 表示升序(+可省略)
@app.route('/users', methods=['GET'])
def get_users():
sort_param = request.args.get('sort', 'created_at')
order_by = []
for field in sort_param.split(','):
if field.startswith('-'):
order_by.append((field[1:], 'desc'))
elif field.startswith('+'):
order_by.append((field[1:], 'asc'))
else:
order_by.append((field, 'asc'))
users = query_users(order_by=order_by)
return jsonify({"data": users})
6.4 完整示例
GET /api/v1/users?page=1&size=20&status=active&sort=-created_at
{
"data": [
{
"id": 100,
"name": "张三",
"status": "active",
"created_at": "2024-01-15T08:30:00Z"
},
{
"id": 99,
"name": "李四",
"status": "active",
"created_at": "2024-01-14T16:45:00Z"
}
],
"pagination": {
"page": 1,
"size": 20,
"total": 156,
"total_pages": 8,
"has_next": true,
"has_prev": false
},
"filters_applied": {
"status": "active"
},
"sort_applied": ["-created_at"]
}
七、错误响应:统一格式,拒绝"黑盒"
7.1 错误响应格式设计
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Invalid email format"
},
{
"field": "age",
"code": "OUT_OF_RANGE",
"message": "Age must be between 18 and 120"
}
],
"timestamp": "2024-01-15T08:30:00Z",
"request_id": "req_abc123xyz",
"documentation_url": "https://api.example.com/docs/errors/VALIDATION_ERROR"
}
}
7.2 错误处理中间件(Flask)
from flask import Flask, jsonify
from werkzeug.exceptions import HTTPException
app = Flask(__name__)
class APIError(Exception):
"""自定义API错误"""
def __init__(self, code, message, status_code=400, details=None):
self.code = code
self.message = message
self.status_code = status_code
self.details = details or []
@app.errorhandler(APIError)
def handle_api_error(error):
"""处理自定义API错误"""
response = {
"error": {
"code": error.code,
"message": error.message,
"details": error.details,
"timestamp": datetime.utcnow().isoformat() + "Z",
"request_id": g.get('request_id', 'unknown')
}
}
return jsonify(response), error.status_code
@app.errorhandler(404)
def handle_not_found(error):
"""处理404错误"""
return jsonify({
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "The requested resource does not exist",
"timestamp": datetime.utcnow().isoformat() + "Z"
}
}), 404
@app.errorhandler(500)
def handle_internal_error(error):
"""处理500错误"""
# 记录详细错误日志
logger.error(f"Internal error: {error}", exc_info=True)
return jsonify({
"error": {
"code": "INTERNAL_ERROR",
"message": "An internal error occurred. Please try again later.",
"timestamp": datetime.utcnow().isoformat() + "Z",
"request_id": g.get('request_id', 'unknown')
}
}), 500
# 使用示例
@app.route('/users', methods=['POST'])
def create_user():
data = request.get_json()
errors = []
if not data.get('email'):
errors.append({"field": "email", "code": "REQUIRED", "message": "Email is required"})
elif not is_valid_email(data['email']):
errors.append({"field": "email", "code": "INVALID_FORMAT", "message": "Invalid email format"})
if not data.get('name'):
errors.append({"field": "name", "code": "REQUIRED", "message": "Name is required"})
if errors:
raise APIError(
code="VALIDATION_ERROR",
message="Request validation failed",
status_code=422,
details=errors
)
# … 创建用户逻辑
八、互动挑战:检查你的API是否符合规范
来,做个小测试,看看你的API设计能得多少分:
| URL 使用名词复数(/users 而非 /getUsers) | ☐ | ☐ |
| URL 使用小写字母和连字符 | ☐ | ☐ |
| 正确使用 HTTP 方法(GET/POST/PUT/PATCH/DELETE) | ☐ | ☐ |
| 状态码使用准确(200/201/204/400/401/403/404/422等) | ☐ | ☐ |
| 错误响应格式统一 | ☐ | ☐ |
| 支持分页、过滤、排序 | ☐ | ☐ |
| 有 API 版本管理策略 | ☐ | ☐ |
| 使用 HTTPS | ☐ | ☐ |
| 实现了限流保护 | ☐ | ☐ |
| 有完善的接口文档 | ☐ | ☐ |
得分:
- 10分:RESTful 大师!
- 7-9分:还不错,继续优化
- 4-6分:还有很大提升空间
- 0-3分:建议重读本文 😄
九、源码获取与思考题
9.1 完整源码获取
本文所有代码示例已整理成完整项目,包含:
- Flask RESTful API 完整实现
- 错误处理中间件
- 分页/过滤/排序工具类
- 单元测试
GitHub 地址: https://github.com/example/restful-api-best-practices
9.2 思考题
场景题:如果你的 API 需要支持批量删除(一次删除多个用户),你会如何设计?是用 DELETE /users?ids=1,2,3 还是 POST /users/batch-delete?为什么?
进阶题:RESTful 原则中提到 HATEOAS(超媒体作为应用状态引擎),你了解吗?在实际项目中你会使用吗?
实战题:设计一个文件上传接口,要求支持断点续传,你会如何设计 URL 和 HTTP 方法?
欢迎在评论区分享你的答案!
十、系列预告
本文是《后端架构设计》系列的第 20 篇,后续还将更新:
- 主题21:API 安全设计:认证、鉴权、防刷最佳实践
- 主题22:GraphQL vs REST:如何选择?
- 主题23:API 性能优化:缓存、压缩、限流实战
- 主题24:微服务架构下的 API 网关设计
- 主题25:OpenAPI 规范:自动生成文档和 SDK
关注不迷路,点赞收藏防丢失!
总结
RESTful API 设计看似简单,但细节决定成败。记住这 10 个核心规范:
好的 API 设计,是工程师的基本功,也是产品的门面。
📌 标签: RESTful, API设计, HTTP, 后端开发, 接口规范, Web开发, 架构设计
💬 互动: 你的 API 设计得多少分?评论区聊聊你踩过的坑!




