Flask is a lightweight WSGI web framework for Python. pip install flask. App: from flask import Flask; app = Flask(__name__). Route: @app.route("/users", methods=["GET"]). JSON response: from flask import jsonify; return jsonify({"key":"val"}), 200. Request body: from flask import request; data = request.get_json(). Form: request.form["field"]. Query: request.args.get("q",""). Path param: @app.route("/users/<int:user_id>"). Blueprint: from flask import Blueprint; bp = Blueprint("users", __name__, url_prefix="/users"); app.register_blueprint(bp). before_request: @app.before_request def auth(): .... after_request: @app.after_request def add_headers(resp): resp.headers["X-Custom"]="v"; return resp. abort: from flask import abort; abort(404). HTTPException: @app.errorhandler(404) def not_found(e): return jsonify({"error":"not found"}), 404. g: from flask import g; g.user = user. send_file: from flask import send_file; return send_file("path/to/file.pdf"). url_for: from flask import url_for; url_for("users.get_user", user_id=1). Factory: def create_app(config=None) -> Flask: app = Flask(__name__); app.config.from_object(config); ...return app. Session: from flask import session; session["user_id"] = uid; session.clear(). Run: flask --app app run --debug. Test: with app.test_client() as c: r = c.get("/users"); assert r.status_code == 200. Claude Code generates Flask REST APIs, Blueprint-structured apps, middleware, and test suites.
CLAUDE.md for Flask
## Flask Stack
- Version: Flask >= 3.0 | pip install flask
- Factory: create_app(config) pattern | app.config.from_object/from_envvar
- Route: @bp.route("/path", methods=["GET","POST"]) | return jsonify({}), status
- Request: request.get_json() | request.args.get("key") | request.form["field"]
- Error: abort(400) | @app.errorhandler(404) def handler(e): return jsonify(...), 404
- Test: app.test_client() | pytest with fixture returning test_client | assert r.json
Flask REST API Pipeline
# app/flask_app.py — Flask app factory, Blueprints, middleware, error handling, testing
from __future__ import annotations
import logging
import os
import time
from dataclasses import asdict, dataclass
from functools import wraps
from typing import Any, Callable
from flask import (
Blueprint,
Flask,
abort,
current_app,
g,
jsonify,
request,
send_file,
)
from flask import Response
from werkzeug.exceptions import HTTPException
log = logging.getLogger(__name__)
# ─────────────────────────────────────────────────────────────────────────────
# 1. Response helpers
# ─────────────────────────────────────────────────────────────────────────────
def ok(data: Any = None, message: str = "ok", status: int = 200) -> Response:
"""
Standard 200 JSON response.
Example:
return ok({"users": users})
return ok(message="created", status=201)
"""
body: dict = {"status": "ok"}
if message != "ok":
body["message"] = message
if data is not None:
body["data"] = data
return jsonify(body), status # type: ignore[return-value]
def created(data: Any = None) -> Response:
"""201 Created response."""
return ok(data=data, status=201)
def no_content() -> Response:
"""204 No Content response."""
return Response(status=204)
def error(message: str, status: int = 400, **extra) -> Response:
"""
Standard error JSON response.
Example:
return error("Invalid email address", 422)
return error("Not found", 404, resource="user", id=42)
"""
body = {"status": "error", "message": message, **extra}
return jsonify(body), status # type: ignore[return-value]
def paginated(items: list, total: int, page: int, per_page: int) -> Response:
"""
Paginated list response.
Example:
return paginated(users, total=250, page=2, per_page=20)
"""
return ok({
"items": items,
"total": total,
"page": page,
"per_page": per_page,
"pages": -(-total // per_page), # ceiling div
})
# ─────────────────────────────────────────────────────────────────────────────
# 2. Request helpers
# ─────────────────────────────────────────────────────────────────────────────
def get_json_body(required: list[str] | None = None) -> dict:
"""
Parse JSON body; abort 400 if not JSON or required fields missing.
Example:
body = get_json_body(required=["email", "password"])
user = create_user(body["email"], body["password"])
"""
data = request.get_json(silent=True)
if data is None:
abort(400, description="Request body must be valid JSON")
if required:
missing = [k for k in required if k not in data]
if missing:
abort(400, description=f"Missing required fields: {', '.join(missing)}")
return data
def get_pagination() -> tuple[int, int]:
"""
Parse ?page= and ?per_page= query params.
Returns (page, per_page) with sensible defaults and max cap.
Example:
page, per_page = get_pagination()
offset = (page - 1) * per_page
"""
try:
page = max(1, int(request.args.get("page", 1)))
per_page = min(100, max(1, int(request.args.get("per_page", 20))))
except ValueError:
abort(400, description="page and per_page must be integers")
return page, per_page
def get_int_param(name: str, default: int | None = None, min_val: int | None = None) -> int | None:
"""
Parse an integer query parameter, abort 400 on invalid value.
Example:
limit = get_int_param("limit", default=50, min_val=1)
"""
raw = request.args.get(name)
if raw is None:
return default
try:
val = int(raw)
except ValueError:
abort(400, description=f"Parameter '{name}' must be an integer")
if min_val is not None and val < min_val:
abort(400, description=f"Parameter '{name}' must be >= {min_val}")
return val
# ─────────────────────────────────────────────────────────────────────────────
# 3. Decorators / middleware
# ─────────────────────────────────────────────────────────────────────────────
def require_json(f: Callable) -> Callable:
"""
Decorator: abort 415 if Content-Type is not application/json.
Example:
@bp.route("/users", methods=["POST"])
@require_json
def create_user(): ...
"""
@wraps(f)
def wrapper(*args, **kwargs):
if not request.is_json:
abort(415, description="Content-Type must be application/json")
return f(*args, **kwargs)
return wrapper
def require_auth(f: Callable) -> Callable:
"""
Decorator: extract Bearer token from Authorization header into g.token.
Abort 401 if missing.
Example:
@bp.route("/profile")
@require_auth
def profile():
token = g.token
"""
@wraps(f)
def wrapper(*args, **kwargs):
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
abort(401, description="Authorization header required (Bearer token)")
g.token = auth_header[7:]
return f(*args, **kwargs)
return wrapper
def log_requests(app: Flask) -> None:
"""
Register before/after request hooks to log timing.
Example:
log_requests(app)
"""
@app.before_request
def _start_timer():
g.request_start = time.perf_counter()
@app.after_request
def _log_response(response: Response) -> Response:
elapsed_ms = (time.perf_counter() - getattr(g, "request_start", time.perf_counter())) * 1000
log.info(
"%s %s → %d (%.1fms)",
request.method, request.path, response.status_code, elapsed_ms,
)
return response
# ─────────────────────────────────────────────────────────────────────────────
# 4. Error handlers
# ─────────────────────────────────────────────────────────────────────────────
def register_error_handlers(app: Flask) -> None:
"""
Register JSON error handlers for common HTTP errors.
Example:
register_error_handlers(app)
"""
@app.errorhandler(HTTPException)
def handle_http_error(exc: HTTPException) -> Response:
return jsonify({
"status": "error",
"code": exc.code,
"error": exc.name,
"message": exc.description,
}), exc.code # type: ignore[return-value]
@app.errorhandler(Exception)
def handle_unexpected(exc: Exception) -> Response:
log.exception("Unhandled exception: %s", exc)
return jsonify({
"status": "error",
"code": 500,
"error": "Internal Server Error",
"message": "An unexpected error occurred",
}), 500 # type: ignore[return-value]
# ─────────────────────────────────────────────────────────────────────────────
# 5. Example Blueprint: Users resource
# ─────────────────────────────────────────────────────────────────────────────
users_bp = Blueprint("users", __name__, url_prefix="/users")
# In-memory store for demo
_USERS: dict[int, dict] = {
1: {"id": 1, "name": "Alice", "email": "[email protected]", "active": True},
2: {"id": 2, "name": "Bob", "email": "[email protected]", "active": True},
}
_NEXT_ID = 3
@users_bp.route("/", methods=["GET"])
def list_users():
"""GET /users — list all users with pagination."""
page, per_page = get_pagination()
all_users = list(_USERS.values())
start = (page - 1) * per_page
subset = all_users[start : start + per_page]
return paginated(subset, total=len(all_users), page=page, per_page=per_page)
@users_bp.route("/<int:user_id>", methods=["GET"])
def get_user(user_id: int):
"""GET /users/<id> — fetch a single user."""
user = _USERS.get(user_id)
if not user:
abort(404, description=f"User {user_id} not found")
return ok(user)
@users_bp.route("/", methods=["POST"])
@require_json
def create_user():
"""POST /users — create a user."""
global _NEXT_ID
body = get_json_body(required=["name", "email"])
user = {"id": _NEXT_ID, "name": body["name"], "email": body["email"], "active": True}
_USERS[_NEXT_ID] = user
_NEXT_ID += 1
return created(user)
@users_bp.route("/<int:user_id>", methods=["PATCH"])
@require_json
def update_user(user_id: int):
"""PATCH /users/<id> — partial update."""
user = _USERS.get(user_id)
if not user:
abort(404, description=f"User {user_id} not found")
body = get_json_body()
for key in ("name", "email", "active"):
if key in body:
user[key] = body[key]
return ok(user)
@users_bp.route("/<int:user_id>", methods=["DELETE"])
def delete_user(user_id: int):
"""DELETE /users/<id>."""
if user_id not in _USERS:
abort(404, description=f"User {user_id} not found")
del _USERS[user_id]
return no_content()
# ─────────────────────────────────────────────────────────────────────────────
# 6. App factory
# ─────────────────────────────────────────────────────────────────────────────
def create_app(config: dict | None = None) -> Flask:
"""
Flask app factory.
Example:
app = create_app()
app = create_app({"TESTING": True, "DEBUG": True})
"""
app = Flask(__name__)
app.config.update({
"SECRET_KEY": os.getenv("SECRET_KEY", "dev-secret-change-in-prod"),
"DEBUG": os.getenv("FLASK_DEBUG", "false").lower() == "true",
"JSON_SORT_KEYS": False,
})
if config:
app.config.update(config)
log_requests(app)
register_error_handlers(app)
app.register_blueprint(users_bp)
@app.route("/health")
def health():
return jsonify({"status": "ok", "service": "flask-api"})
return app
# ─────────────────────────────────────────────────────────────────────────────
# Demo / test run
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
app = create_app({"DEBUG": True})
print("=== Flask in-process test client demo ===")
with app.test_client() as client:
print("\n--- GET /health ---")
r = client.get("/health")
print(f" {r.status_code}: {r.json}")
print("\n--- GET /users/ (paginated) ---")
r = client.get("/users/?page=1&per_page=5")
print(f" {r.status_code}: total={r.json['data']['total']}, items={len(r.json['data']['items'])}")
print("\n--- GET /users/1 ---")
r = client.get("/users/1")
print(f" {r.status_code}: {r.json['data']}")
print("\n--- POST /users/ ---")
r = client.post("/users/", json={"name": "Carol", "email": "[email protected]"})
print(f" {r.status_code}: {r.json['data']}")
new_id = r.json["data"]["id"]
print("\n--- PATCH /users/<new_id> ---")
r = client.patch(f"/users/{new_id}", json={"name": "Caroline"})
print(f" {r.status_code}: name={r.json['data']['name']!r}")
print("\n--- DELETE /users/<new_id> ---")
r = client.delete(f"/users/{new_id}")
print(f" {r.status_code} No Content: {r.status_code == 204}")
print("\n--- 404 error handler ---")
r = client.get("/users/9999")
print(f" {r.status_code}: {r.json['message']!r}")
print("\n--- 400 missing fields ---")
r = client.post("/users/", json={"name": "NoEmail"})
print(f" {r.status_code}: {r.json['message']!r}")
print("\nTo run the dev server:")
print(" flask --app app.flask_app:create_app run --debug")
For the FastAPI alternative — FastAPI uses Python type annotations and Pydantic to auto-generate OpenAPI docs, async request handlers, and automatic request/response validation; Flask is synchronous by default, requires manual validation, but has a much larger extension ecosystem (Flask-SQLAlchemy, Flask-Login, Flask-Admin) and simpler mental model — use FastAPI for new APIs where you want automatic OpenAPI docs, async endpoints, and tight Pydantic integration, Flask for projects leveraging its mature extension ecosystem or when the team prefers its simpler explicit-over-magic design. For the Django alternative — Django is a batteries-included framework with ORM, admin panel, authentication, migrations, forms, and a convention-heavy project structure; Flask is micro-framework with no ORM or admin, requiring explicit library choices for each concern but offering more architectural freedom — use Django for content-heavy or data-admin applications where the built-in ORM and admin panel save significant time, Flask for APIs or applications where you don’t need Django’s built-ins and want full control over your stack choices. The Claude Skills 360 bundle includes Flask skill sets covering ok()/created()/no_content()/error()/paginated() response helpers, get_json_body()/get_pagination()/get_int_param() request helpers, require_json/require_auth decorators, log_requests()/register_error_handlers() middleware, users_bp Blueprint with CRUD routes, and create_app() factory pattern. Start with the free tier to try Flask REST API and web application code generation.