API Design & REST

Building an API with Flask

40 分钟

A JSON blueprint

from flask import Blueprint, jsonify, request, url_for
from pydantic import BaseModel, Field, ValidationError

api = Blueprint("api_v1", __name__, url_prefix="/v1")

class ProjectIn(BaseModel):
    title: str = Field(min_length=1, max_length=200)
    description: str = Field(default="", max_length=5000)
    category: str

def project_out(p):
    return {"id": str(p.id), "title": p.title, "description": p.description,
            "category": p.category, "created_at": p.created_at.isoformat() + "Z"}

Pydantic validates input and gives detailed error messages; a plain function controls exactly what goes out, so internal fields (password hashes, internal notes) never leak.

Endpoints

@api.get("/projects")
def list_projects():
    limit = min(int(request.args.get("limit", 20)), 100)
    after = request.args.get("cursor", type=int)
    stmt = select(Project).where(Project.is_published.is_(True)).order_by(Project.id.desc())
    if after:
        stmt = stmt.where(Project.id < after)
    rows = db.session.scalars(stmt.limit(limit + 1)).all()
    has_more = len(rows) > limit
    rows = rows[:limit]
    return jsonify(data=[project_out(p) for p in rows],
                   has_more=has_more,
                   next_cursor=str(rows[-1].id) if has_more else None)

@api.post("/projects")
@require_api_key(scope="projects:write")
def create_project():
    try:
        data = ProjectIn.model_validate(request.get_json(force=False) or {})
    except ValidationError as exc:
        return api_error(422, "validation_failed", "Invalid project.",
                         fields={e["loc"][0]: e["msg"] for e in exc.errors()})
    project = Project(**data.model_dump(), owner_id=g.api_user.id)
    db.session.add(project)
    db.session.commit()
    resp = jsonify(project_out(project))
    resp.status_code = 201
    resp.headers["Location"] = url_for("api_v1.get_project", project_id=project.id)
    return resp

Central error handling

def api_error(status, code, message, **extra):
    resp = jsonify(error={"code": code, "message": message, **extra})
    resp.status_code = status
    return resp

@api.errorhandler(404)
def not_found(_):
    return api_error(404, "not_found", "Resource not found.")

@api.errorhandler(Exception)
def unexpected(exc):
    current_app.logger.exception("API error")
    return api_error(500, "internal_error", "Something went wrong.")

CSRF and APIs

Session-cookie-based browser forms need CSRF protection. Token-authenticated APIs (API keys or bearer tokens in the Authorization header) are not vulnerable to CSRF in the same way, so API blueprints are often exempted from CSRF only when they do not accept cookie sessions.

CORS

Browsers block cross-origin requests unless the API allows them. Allow only the origins that need access (flask-cors with an explicit origin list); never combine * with credentials.

Exercise

Implement list, get, create, update (PATCH) and delete endpoints for projects with Pydantic validation, cursor pagination, a consistent error format and tests for each status code.

Check yourself

  • Why serialise with an explicit output function?
  • When can an API blueprint skip CSRF protection?
  • Why not allow CORS from * with credentials?

你的进度只保存在此浏览器中。

提交审核(10 积分)