Claude Fable 5.1 & GPT-6 Astra packages are live

Flask

Free

Flask application structure — the app factory, blueprints by feature, config classes, extension initialisation, request lifecycle hooks, error…

247 lines8.7 KB Mistral Backend
targetModels
Mistral Medium 3.5Mistral Large 3Mistral Small 4Mistral FamilyFuture Mistral Models
name
flask
category
Backend
description
Flask application structure — the app factory, blueprints by feature, config classes, extension initialisation, request lifecycle hooks, error handlers, and when to reach for FastAPI instead.
license
MIT
author
Agent.md maintainers
last-verified
reviewed-by
unreviewed
<!-- Generated from models/_canonical by scripts/build-model-variants.js. Edit the canonical source, not this file. Behavioural profile for Mistral: scripts/model-profiles.json -->

#How to apply this file

Each section opens with one imperative line; apply every rule in the section it introduces. Do not summarise or skip a section.


#Purpose

Rules for a Flask service that is testable and does not collapse into one app.py. Flask is a microframework: it decides nothing for you, so every structural decision below is one it left open.

Python conventions are Backend/python-conventions; if the service is mostly JSON with typed models, read the last section before choosing Flask.


#The app factory

[INST] Apply every rule in this section: The app factory. [/INST]

python
# app/__init__.py
def create_app(config: type[Config] = ProdConfig) -> Flask:
    app = Flask(__name__)
    app.config.from_object(config)
    db.init_app(app)                       # extensions are module-level, bound here
    app.register_blueprint(orders_bp, url_prefix="/v1/orders")
    register_error_handlers(app)
    return app

# wsgi.py — the only place an app is instantiated for serving
app = create_app()
  • No app = Flask(__name__) at module level anywhere else. A global app is configured at import time, cannot be built twice with different config, and makes every test share state.
  • Extensions (SQLAlchemy(), Migrate(), LoginManager()) are created without an app and bound in the factory with init_app.
  • Tests call create_app(TestConfig).

#Blueprints by feature

[INST] Apply every rule in this section: Blueprints by feature. [/INST]

scss
app/
  orders/
    __init__.py     # bp = Blueprint("orders", __name__)
    routes.py       # HTTP only
    services.py     # rules; no `request` imported here
    schemas.py      # marshmallow / pydantic
  extensions.py     # db = SQLAlchemy(); migrate = Migrate()
python
@bp.post("")
def create_order():
    data = OrderSchema().load(request.get_json(force=False, silent=False))
    order = services.create_order(actor=current_user, **data)
    return OrderSchema().dump(order), 201

The rule: request and g do not leave routes.py. A service that reads request.json cannot run from a CLI command or a worker. Pass values in.


#Config

[INST] Apply every rule in this section: Config. [/INST]

python
class Config:
    SECRET_KEY = os.environ["SECRET_KEY"]                   # fail fast if missing
    SQLALCHEMY_DATABASE_URI = os.environ["DATABASE_URL"]
    MAX_CONTENT_LENGTH = 1 * 1024 * 1024                   # 1 MiB request cap
    SESSION_COOKIE_SECURE = True
    SESSION_COOKIE_HTTPONLY = True
    SESSION_COOKIE_SAMESITE = "Lax"

class TestConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = "sqlite://"
  • os.environ["X"] not os.environ.get("X") for required values; a KeyError at boot beats a None secret key in production.
  • MAX_CONTENT_LENGTH is unset by default — set it, or a single request can exhaust memory.
  • Never app.config["DEBUG"] = True in a config that could reach production; the Werkzeug debugger is remote code execution.

#Request lifecycle

[INST] Apply every rule in this section: Request lifecycle. [/INST]

python
@app.before_request
def attach_request_id():
    g.request_id = request.headers.get("X-Request-Id") or uuid4().hex

@app.teardown_appcontext
def shutdown_session(exc):
    db.session.remove()            # return the connection; roll back on error
  • g is per-request; use it for the request id, the current tenant, timing. Module globals for the same purpose leak between requests under threading.
  • teardown_appcontext runs even when the view raised — put cleanup there, not in after_request, which is skipped on unhandled exceptions.
  • Behind a proxy, wrap with ProxyFix(app.wsgi_app, x_for=1, x_proto=1) with the exact hop count; otherwise request.remote_addr is the proxy and rate limits key on one address for everyone. → API/rate-limiting

#Error handlers

[INST] Apply every rule in this section: Error handlers. [/INST]

python
def register_error_handlers(app: Flask) -> None:
    @app.errorhandler(ValidationError)
    def bad_input(e):
        return {"error": "validation", "fields": e.messages}, 400

    @app.errorhandler(HTTPException)
    def http_error(e):
        return {"error": e.name.lower().replace(" ", "_")}, e.code

    @app.errorhandler(Exception)
    def unhandled(e):
        app.logger.exception("unhandled", extra={"request_id": g.get("request_id")})
        return {"error": "internal"}, 500
  • Register a handler for HTTPException so abort(404) returns your JSON envelope, not Werkzeug's HTML page.
  • The catch-all logs the traceback with the request id and returns a fixed body. A default 500 in a JSON API returns HTML to the client.
  • Domain exceptions are raised in services and mapped here, once. → Backend/error-handling

#Testing

[INST] Apply every rule in this section: Testing. [/INST]

python
@pytest.fixture
def client():
    app = create_app(TestConfig)
    with app.app_context():
        db.create_all()
        yield app.test_client()
        db.drop_all()

def test_create_order_requires_items(client):
    r = client.post("/v1/orders", json={"items": []})
    assert r.status_code == 400
    assert r.get_json()["error"] == "validation"

app.test_client() drives the WSGI app in-process — no port, parallel-safe. Assert the error envelope, not just the status. → Testing/pytest


#Flask or FastAPI

[INST] Apply every rule in this section: Flask or FastAPI. [/INST]

Choose Flask whenChoose FastAPI when
Server-rendered HTML with Jinja and sessionsThe service is JSON-first
A large existing Flask codebase and teamYou want typed models, DI and OpenAPI built in
Sync everything, simple deployment (gunicorn)Endpoints await async drivers or fan out I/O
Extensions you already depend onYou would otherwise bolt on marshmallow + apispec

Flask 2.x can async def a view, but it runs each one in a thread via asgiref; it is not an async framework. If half your views are async, you picked the wrong one. → Backend/fastapi


#Anti-patterns

[INST] Apply every rule in this section: Anti-patterns. [/INST]

Anti-patternWhy it failsFix
Module-level app = Flask(__name__)Untestable, single configApp factory
Extensions bound at importSame problem, plus circular importsinit_app in the factory
request used inside servicesNot callable from CLI or workersPass values in
os.environ.get("SECRET_KEY")None secret in productionos.environ["..."]
No MAX_CONTENT_LENGTHMemory exhaustion by one requestSet a cap
debug=True reachable in prodRemote code execution via debuggerNever in a prod config
Module globals for request stateCross-request leakageg
Cleanup in after_requestSkipped on exceptionsteardown_appcontext
No HTTPException handlerHTML error pages from a JSON APIRegister one
Bare except returning 200Errors disappearHandler that logs and returns 500
No ProxyFix behind a proxyWrong client IP everywhereProxyFix with hop count
Fifty routes in app.pyUnnavigableBlueprints by feature
async def views everywhereThread-per-view, not asyncFastAPI

#Checklist

  • Verify: create_app(config) factory; no module-level app
  • Verify: Extensions created bare and bound with init_app
  • Verify: One blueprint per feature with routes.py/services.py split
  • Verify: request and g never imported by services
  • Verify: Required config read with os.environ["X"]
  • Verify: MAX_CONTENT_LENGTH and secure cookie flags set
  • Verify: Debug mode impossible in production config
  • Verify: Request id attached in before_request and logged
  • Verify: Session cleanup in teardown_appcontext
  • Verify: ProxyFix applied with the exact proxy count
  • Verify: Handlers registered for ValidationError, HTTPException, Exception
  • Verify: Tests use create_app(TestConfig) and test_client()
  • Verify: Framework choice justified against the Flask/FastAPI table