REST APIs

Build JSON-returning REST-style APIs with Flask. Flask itself is light enough — blueprints for versioning are all you need, no extra framework required.

Basic Structure

When a view returns a dict/list, Flask serializes it to JSON automatically and sets Content-Type: application/json:

from flask import Blueprint, request

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

@api.get("/users")
def list_users():
    return [{"id": 1, "name": "Alice"}]          # Lists can be returned directly (Flask 2.2+)

@api.get("/users/<int:user_id>")
def get_user(user_id):
    user = find_user(user_id)
    if user is None:
        return {"error": "Not Found"}, 404
    return user

@api.post("/users")
def create_user():
    data = request.get_json()
    if not data or "name" not in data:
        return {"error": "name is required"}, 400
    user = save_user(data)
    return user, 201

@api.delete("/users/<int:user_id>")
def delete_user(user_id):
    remove_user(user_id)
    return "", 204

Request Body Validation

request.get_json() raises 415/400 when the request isn't JSON; pass silent=True to get None instead and handle it yourself. For production projects, use Pydantic or Marshmallow for structured validation:

from pydantic import BaseModel, ValidationError

class UserIn(BaseModel):
    name: str
    email: str
    age: int | None = None

@api.post("/users")
def create_user():
    try:
        payload = UserIn.model_validate(request.get_json(silent=True) or {})
    except ValidationError as e:
        return {"error": "validation failed", "detail": e.errors()}, 422
    ...

Pagination

Establish query-parameter conventions and a uniform response shape:

@api.get("/posts")
def list_posts():
    page = request.args.get("page", 1, type=int)
    per_page = request.args.get("per_page", 20, type=int)
    pagination = Post.query.order_by(Post.id.desc()).paginate(
        page=page, per_page=min(per_page, 100), error_out=False
    )
    return {
        "items": [p.to_dict() for p in pagination.items],
        "total": pagination.total,
        "page": page,
        "per_page": pagination.per_page,
    }

Uniform Error Responses

Make errors under the API blueprint return JSON instead of HTML error pages:

from werkzeug.exceptions import HTTPException

@api.errorhandler(HTTPException)
def api_http_error(e):
    return {"error": e.name, "message": e.description}, e.code

Authentication

Common options:

  • Session cookie: simplest for same-origin, integrated frontend/backend apps.
  • Token (Bearer): clients send Authorization: Bearer <token>; the server verifies it and stores the user on g.current_user.
  • JWT: use the Flask-JWT-Extended extension for refresh tokens and expiry management.
from functools import wraps
from flask import g, request

def auth_required(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        token = (request.headers.get("Authorization") or "").removeprefix("Bearer ").strip()
        user = verify_token(token)
        if user is None:
            return {"error": "Unauthorized"}, 401
        g.current_user = user
        return f(*args, **kwargs)
    return wrapper

Cross-Origin Requests (CORS)

When the frontend is deployed on a different domain, enable CORS:

pip install flask-cors
from flask_cors import CORS

CORS(api, origins=["https://app.example.com"])   # Open only the API blueprint, restrict origins

Versioning Advice

  • URL-prefix versioning (/api/v1/) is the most intuitive — one blueprint per version.
  • Bump the version only for breaking changes (removed fields, changed semantics); adding fields stays backward-compatible.