RESTful API 开发:为 App/小程序提供轻量 JSON 接口

📂 所属阶段:第五阶段 — 高级进阶(性能与架构)
🔗 相关章节:Flask 上下文深挖 · 数据验证


0. 为什么选 REST + Flask?

现在前后端分离架构几乎成了主流:移动App、小程序、Vue/React 单页应用,都需要统一、无状态、资源导向的后端接口——RESTful 刚好符合这些要求,而 Flask 轻量、灵活的特性,又能让我们快速搭建符合规范的接口原型,甚至迭代成生产可用的服务。

这篇文章会通过一个「文章管理API」的完整案例,带你掌握 Flask RESTful JSON 开发的核心。


1. RESTful 设计核心:资源 + 状态码

1.1 URL 只能是「资源名词」

URL 是用来定位资源的,不能包含「获取、创建、删除」这类操作动词——这些都交给 HTTP 方法去做。

// ❌ 反例:传统「动作+资源」的URL
GET /get_articles      → 动词 get
POST /do_login          → 动词 do
GET /getArticleDetail?id=42 → 资源没放在URL层级里

// ✅ 正例:RESTful 规范URL
GET    /api/articles     → 获取已发布文章列表(分页、可筛选)
POST   /api/articles     → 创建新文章
GET    /api/articles/42  → 获取ID为42的单篇文章
PUT    /api/articles/42  → 全量更新ID为42的文章
DELETE /api/articles/42  → 删除ID为42的文章

💡 小细节:

  • 统一加前缀 /api 区分后端接口和前端页面路由
  • 资源用复数articles而非article),符合「列表是资源的集合」的逻辑
  • 层级不要超过3层(比如 /api/articles/42/comments可以,/api/users/1/categories/5/articles就太复杂了,考虑用筛选参数或反向关联URL)

1.2 准确使用 HTTP 状态码

状态码是机器和人都能看懂的响应标识,别全返回200或500。这里只列RESTful开发最常用的:

状态码英文标签适用场景
200OK查询/全量更新成功(有返回数据)
201Created创建资源成功(返回空或新建资源的标识)
204No Content删除/批量清空成功(必须无返回体
400Bad Request请求参数格式错误(比如JSON解析失败、必填字段缺类型)
401Unauthorized未登录/登录过期(令牌失效)
403Forbidden已登录但无权限操作此资源
404Not Found请求的资源ID不存在、路由拼写错误
422Unprocessable Entity参数格式对但验证逻辑不通过(比如用户名太短、分类不存在)
500Internal Server Error服务器代码出错(生产环境别把错误详情返回给前端!)

2. 用 Flask 实现基础文章管理 API

我们假设项目已经有了用户认证(Flask-Login)、数据库(SQLAlchemy)、表单验证(Flask-WTF 或 Marshmallow)的基础,这里只讲API部分的核心代码。


2.1 先创建 API 蓝图

用蓝图把接口和页面路由、模板分开,更符合模块化开发的规范:

# app/api/__init__.py
from flask import Blueprint

# 加前缀 /api,所有子路由都会自动带上
api_bp = Blueprint("api", __name__, url_prefix="/api")

# 延迟导入子路由模块,避免循环引用
from . import articles, auth, users

2.2 完整的文章 CRUD 接口

# app/api/articles.py
from flask import jsonify, request, abort
from flask_login import login_required, current_user
from app.api import api_bp
from app.extensions import db
from app.models import Article, Category
from app.forms import ArticleForm
from app.utils import render_markdown


# ------------------------------
# 获取文章列表(分页+分类筛选)
# ------------------------------
@api_bp.route("/articles", methods=["GET"])
def list_articles():
    # 1. 从URL查询参数获取输入
    page = request.args.get("page", 1, type=int)  # 默认第1页
    per_page = request.args.get("per_page", 20, type=int)  # 默认每页20条
    category_id = request.args.get("category", type=int)  # 可选分类筛选

    # 2. 构建查询语句:只返回已发布的文章
    query = Article.query.filter_by(is_published=True)
    if category_id:
        query = query.filter_by(category_id=category_id)

    # 3. 使用SQLAlchemy的paginate实现分页(error_out=False时,越界返回空列表而非404)
    pagination = query.order_by(Article.created_at.desc())\
        .paginate(page=page, per_page=per_page, error_out=False)

    # 4. 组装返回的JSON数据
    return jsonify({
        "data": [
            {
                "id": a.id,
                "title": a.title,
                "summary": a.summary,
                "author": {"id": a.author.id, "name": a.author.username},
                "views": a.views,
                "created_at": a.created_at.isoformat(),  # 统一用ISO8601时间格式
            }
            for a in pagination.items
        ],
        "pagination": {
            "total": pagination.total,  # 总文章数
            "page": page,  # 当前页
            "pages": pagination.pages,  # 总页数
            "has_next": pagination.has_next,  # 是否有下一页
            "has_prev": pagination.has_prev,  # 是否有上一页
        }
    })


# ------------------------------
# 获取单篇文章详情(自动增加阅读量)
# ------------------------------
@api_bp.route("/articles/<int:article_id>", methods=["GET"])
def get_article(article_id):
    # 1. 查找文章,找不到直接返回404(Flask的get_or_404会自动处理)
    article = Article.query.get_or_404(article_id)

    # 2. 业务逻辑:每次打开详情页阅读量+1
    article.views += 1
    db.session.commit()

    # 3. 返回更详细的信息(包含完整内容、HTML渲染后的内容、作者头像、分类等)
    return jsonify({
        "id": article.id,
        "title": article.title,
        "content": article.content,
        "content_html": render_markdown(article.content),  # 把Markdown转成HTML返回给前端
        "summary": article.summary,
        "author": {
            "id": article.author.id,
            "name": article.author.username,
            "avatar": article.author.get_avatar(),
        },
        "category": {"id": article.category.id, "name": article.category.name} if article.category else None,
        "views": article.views,
        "created_at": article.created_at.isoformat(),
        "updated_at": article.updated_at.isoformat(),
    })


# ------------------------------
# 创建新文章(需要登录)
# ------------------------------
@api_bp.route("/articles", methods=["POST"])
@login_required  # Flask-Login装饰器,验证用户是否已登录
def create_article():
    # 1. 检查请求体是否为有效的JSON
    data = request.get_json()
    if not data:
        abort(400, description="请求体必须是有效的JSON格式")

    # 2. 使用表单验证(Flask-WTF)或Marshmallow验证数据
    form = ArticleForm(data=data)
    if not form.validate():
        # 验证失败返回422和具体错误
        return jsonify({"error": "数据验证失败", "details": form.errors}), 422

    # 3. 组装数据并保存到数据库
    article = Article(
        title=form.title.data,
        content=form.content.data,
        summary=form.summary.data,
        category_id=form.category_id.data or None,
        author=current_user,
    )
    db.session.add(article)
    db.session.commit()

    # 4. 返回201状态码和新建文章的ID
    return jsonify({"id": article.id, "message": "文章创建成功"}), 201


# ------------------------------
# 全量/部分更新文章(需要登录且有权限)
# ------------------------------
@api_bp.route("/articles/<int:article_id>", methods=["PUT"])
@login_required
def update_article(article_id):
    # 1. 查找文章
    article = Article.query.get_or_404(article_id)

    # 2. 权限验证:只有作者本人或管理员能修改
    if article.author_id != current_user.id and not current_user.is_admin:
        abort(403, description="无权修改此文章")

    # 3. 获取请求JSON
    data = request.get_json()
    if not data:
        abort(400, description="请求体不能为空")

    # 4. 只更新请求中存在的字段(部分更新用PATCH,不过PUT这里也兼容了)
    allowed_fields = ["title", "content", "summary", "is_published", "category_id"]
    for field in allowed_fields:
        if field in data:
            setattr(article, field, data[field])

    # 5. 保存并返回
    db.session.commit()
    return jsonify({"message": "文章更新成功"}), 200


# ------------------------------
# 删除文章(需要登录且有权限)
# ------------------------------
@api_bp.route("/articles/<int:article_id>", methods=["DELETE"])
@login_required
def delete_article(article_id):
    # 1. 查找文章
    article = Article.query.get_or_404(article_id)

    # 2. 权限验证
    if article.author_id != current_user.id and not current_user.is_admin:
        abort(403)

    # 3. 删除并保存
    db.session.delete(article)
    db.session.commit()

    # 4. 删除成功必须返回204状态码,且不能有返回体
    return "", 204

3. 统一的 API 错误处理

刚才的代码里用了 abort() 和直接返回状态码,但 Flask 默认的错误页面是 HTML 格式的——这对 API 来说很不友好。我们需要给 API 蓝图注册专属的 JSON 错误处理器

# app/api/errors.py
from flask import jsonify
from app.api import api_bp


# 定义统一的错误响应函数
def _api_error_response(code: int, message: str, details=None):
    """
    生成标准化的 JSON 错误响应
    :param code: HTTP 状态码
    :param message: 简洁的错误提示(给前端展示用)
    :param details: 详细的错误信息(可选,调试用,生产环境可隐藏)
    """
    response_body = {"error": message}
    # 生产环境可以把下面这行删掉
    if details:
        response_body["details"] = details
    return jsonify(response_body), code


# 注册蓝图专属的错误处理器
@api_bp.errorhandler(400)
def bad_request(e):
    return _api_error_response(400, str(e.description) or "请求参数格式错误")


@api_bp.errorhandler(401)
def unauthorized(e):
    return _api_error_response(401, "未登录或登录过期,请重新登录")


@api_bp.errorhandler(403)
def forbidden(e):
    return _api_error_response(403, str(e.description) or "权限不足,无法执行此操作")


@api_bp.errorhandler(404)
def not_found(e):
    return _api_error_response(404, str(e.description) or "请求的资源不存在")


@api_bp.errorhandler(422)
def validation_error(e):
    return _api_error_response(422, "数据验证失败", details=e.description)


@api_bp.errorhandler(500)
def server_error(e):
    # 生产环境绝对不能返回 str(e) 或 e.description,会暴露代码漏洞!
    return _api_error_response(500, "服务器内部错误,请稍后重试")

💡 记得在 app/api/__init__.py 里延迟导入 errors 模块,不然错误处理器不会生效:

# 修改后的 app/api/__init__.py
from flask import Blueprint

api_bp = Blueprint("api", __name__, url_prefix="/api")

from . import articles, auth, users
from . import errors  # 延迟导入,在子路由之后

4. 核心要点速查

4.1 常用 Flask API 函数/方法

代码片段作用
jsonify(dict/list)把 Python 对象转成 JSON 格式的响应
request.get_json()获取请求体中的 JSON 数据(如果不是JSON会返回None)
request.args.get(key, default, type)获取 URL 查询参数(比如 ?page=2 里的 page
abort(code, description)立即中断请求,返回指定状态码的错误
Model.query.paginate()SQLAlchemy 分页查询

4.2 RESTful 设计避坑指南

  1. ❌ 别在 URL 里加版本号前缀?不——可以加,比如 /api/v1/articles,方便后续接口迭代不影响旧版本
  2. ❌ 别全用 GET 或 POST
  3. ❌ 别返回 HTML 给 API
  4. ✅ 统一用 ISO8601 时间格式(比如 2024-05-20T12:34:56Z 或带时区的本地时间)
  5. ✅ 分页、排序、筛选用 URL 查询参数
  6. ✅ 删除成功必须返回 204 且无返回体

5. 扩展方向

如果要把这个 API 升级成生产可用的,可以继续学习:

  1. 接口文档自动生成:用 Flask-RESTXFlask-APISpec 生成 Swagger 文档
  2. JWT 令牌认证:Flask-Login 只适合有 Session 的场景,移动App/小程序推荐用 Flask-JWT-Extended
  3. 接口限流:用 Flask-Limiter 防止恶意请求刷接口
  4. 数据验证优化:用 Marshmallow 替代 Flask-WTF,更适合 API 的验证(不需要 CSRF 保护)
  5. API 缓存:用 Flask-Caching 缓存文章列表等高频查询的数据

🔗 扩展阅读