RESTful API development: Provide lightweight JSON interface for App/mini-program

📂 Stage: Stage 5 - Advanced Advancement (Performance and Architecture) 🔗 Related chapters: Flask 上下文深挖 · 数据验证

0. Why choose REST + Flask?

Today's front-end and back-end separation architecture has become mainstream: whether it is a mobile app, a WeChat applet, or a Vue/React single-page application, a set of unified, stateless, and resource-centered back-end interfaces is needed. The RESTful style just meets these requirements, and the lightweight and flexible nature of Flask allows us to quickly build a standardized interface prototype and then smoothly iterate to production use.

This article will take you step by step through a complete "Article Management API" case to master the core skills of developing RESTful JSON interfaces with Flask.

1. RESTful design core: resources + status code

1.1 URL can only be "resource noun"

The only task of the URL is to identify the resource, and there should be no action words such as "get, create, delete" in it - these operations are all expressed by HTTP methods.

// ❌ 反例:动词出现在 URL 里
GET /get_articles          → 动词 get
POST /do_login             → 动词 do
GET /getArticleDetail?id=42 → 资源层级不清晰

// ✅ 正例: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 的文章

💡 A few practical details:

  • For unified use/apiPrefix to easily distinguish back-end interfaces and front-end routes.
  • Use plural form for resource names (articlesinstead ofarticle), because a list is a collection of resources.
  • No more than 3 layers (e.g./api/articles/42/commentsNo problem, but/api/users/1/categories/5/articlesIt is too complicated, then you can use query parameters or reverse correlation interface instead).

1.2 Accurate use of HTTP status codes

Status code is a response mark that can be read by both humans and machines. Never return it in all situations.200or500. Here are some of the most commonly used ones in RESTful development:

Status codeEnglish nameUsage scenarios
200OKQuery or full update was successful and data was returned
201CreatedNew resource created successfully, usually returns the identification of the new resource (such as id)
204No ContentDelete, clear and other operations are successful, The response body must be empty
400Bad RequestThe request parameter format is incorrect (such as JSON parsing failure, missing required fields)
401UnauthorizedNot logged in or the token has expired (the credentials are invalid)
403ForbiddenLogged in but no operation permission
404Not FoundThe resource to be accessed does not exist, or the route is written incorrectly
422Unprocessable EntityThe parameter format is correct but the business verification fails (for example, the user name is too short, the category does not exist)
500Internal Server ErrorServer code error (Do not return error details to the front end in a production environment!)

2. Use Flask to implement basic article management API

It is assumed here that the project already has the foundation of user authentication (Flask-Login), database (SQLAlchemy) and form validation (Flask-WTF or Marshmallow). This article only focuses on the core code of the API part.

2.1 First create the API blueprint

Using blueprints to separate interfaces from page routing and templates is more in line with modular development habits:

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

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

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

2.2 Complete article CRUD interface

Below is the full implementation of the article management API, including paginated lists, details, creation, update and deletion.

# 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. 从查询参数中提取输入
    page = request.args.get("page", 1, type=int)
    per_page = request.args.get("per_page", 20, type=int)
    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. 按时间倒序,然后分页(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, "名称": 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, # 是否有上一页
        }
    })


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

    # 2. 业务逻辑:每次查看详情,阅读数加 1
    article.views += 1
    db.session.commit()

    # 3. 返回详细信息(包含 Markdown 渲染后的 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():
        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):
    article = Article.query.get_or_404(article_id)

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

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

    # 允许更新的字段白名单(用部分更新兼容 PUT / PATCH)
    allowed_fields = ["title", "content", "summary", "is_published", "category_id"]
    for field in allowed_fields:
        if field in data:
            setattr(article, field, data[field])

    db.session.commit()
    return jsonify({"message": "文章更新成功"}), 200


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

    if article.author_id != current_user.id and not current_user.is_admin:
        abort(403)

    db.session.delete(article)
    db.session.commit()

    # 删除成功必须返回 204,且不能带响应体
    return "", 204

3. Unified API error handling

In the above code, we directly useabort()and manual return status codes. But Flask's default error response is in HTML format, which is very unfriendly to the API. We need to register a specialized JSON error handler with the API blueprint.

# 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: 详细的错误信息(可选,生产环境可以隐藏)
    """
    body = {"error": message}
    # 开发阶段可返回细节,生产环境建议去掉下面这行
    if details:
        body["details"] = details
    return jsonify(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):
    # 生产环境绝对不要返回具体的错误信息!
    return _api_error_response(500, "服务器内部错误,请稍后重试")

💡 Don’t forget toapp/api/__init__.pyImport the error handling module here, otherwise these processors will not take effect:

# 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. Quick check on core points

4.1 Commonly used Flask API shortcut methods

Code snippetFunction
jsonify(dict/list)Convert Python object to JSON formatted response
request.get_json()Get the JSON data in the request body (returned if it is not JSONNone
request.args.get(key, default, type)Get URL query parameters, such as?page=2inpage
abort(code, description)Interrupt request and directly return the specified error status code
Model.query.paginate()SQLAlchemy’s paging query method

4.2 Guide to avoid pitfalls in RESTful design

  1. You can add the version number as a prefix: For example/api/v1/articles, which will not affect the old version of the client when upgrading the interface.
  2. Don’t just use GET and POST: choose the appropriate HTTP method (GET/POST/PUT/DELETE/PATCH) based on the operation.
  3. Never return HTML to the API: All responses should be JSON.
  4. Uniform Time Format: It is recommended to use ISO8601 (for example2024-05-20T12:34:56Zor local time with time zone).
  5. Paging, sorting, and filtering are all placed in query parameters: For example?page=1&sort=created_at
  6. 204 must be returned after successful deletion, and the response body cannot be included.

5. Extension direction

If you want to upgrade this API into a truly production-ready service, you can continue to learn the following content:

  1. Interface document automatically generated: useFlask-RESTXorFlask-APISpecGenerate Swagger documentation directly.
  2. JWT token authentication: Flask-Login relies on Session and is suitable for the Web, while mobile Apps and small programs are more suitable for use.Flask-JWT-ExtendedImplement stateless authentication.
  3. Interface current limit: passedFlask-LimiterPrevent malicious brush requests.
  4. Data validation optimization: useMarshmallowReplaces Flask-WTF, which is more focused on serialization/deserialization of APIs and does not require CSRF protection.
  5. API Cache: useFlask-CachingCache high-frequency queries (such as article lists) to reduce database pressure.

🔗 Extended reading