欢迎光临
我们一直在努力

后端技术20-你的RESTful API可能并不RESTful!这10个规范你遵守了吗?从混乱到规范:RESTful API设计的完整 checklist

程序员生存指南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 个核心规范:

  • 资源导向:用名词,不用动词
  • URL 规范:小写、复数、连字符分隔
  • HTTP 方法:GET/POST/PUT/PATCH/DELETE 各司其职
  • 状态码:准确使用 2xx/3xx/4xx/5xx
  • 版本管理:推荐 URL 路径方式
  • 分页过滤:别让 API 变成数据黑洞
  • 错误格式:统一、详细、可追溯
  • HTTPS:强制使用,安全第一
  • 限流保护:防止滥用
  • 接口文档:及时更新,方便协作
  • 好的 API 设计,是工程师的基本功,也是产品的门面。


    📌 标签: RESTful, API设计, HTTP, 后端开发, 接口规范, Web开发, 架构设计

    💬 互动: 你的 API 设计得多少分?评论区聊聊你踩过的坑!

    赞(0)
    未经允许不得转载:171主机测评 » 后端技术20-你的RESTful API可能并不RESTful!这10个规范你遵守了吗?从混乱到规范:RESTful API设计的完整 checklist
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址