Flask Apps skill for Codex

From one app.py to a testable Flask factory. Codex turns Flask code into an app factory with config from prefixed environment variables, feature blueprints, and extensions bound through init_app so tests and CLI commands get fresh instances. It explains and fixes application and request context errors, scopes queries by owner, returns JSON errors for APIs, reviews Alembic migrations, and prepares gunicorn and ProxyFix settings for production.

  • Backend & APIs
  • $flask-apps
  • 271 lines
  • 2 files
  • ~2,200 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- flask-apps

Installs to ~/.agents/skills/flask-apps/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $flask-apps, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Structures Flask 3 applications with the app factory pattern - create_app with config objects and prefixed environment variables, blueprints per feature, extensions initialised with init_app, Flask-SQLAlchemy 3 and Flask-Migrate, application and request context rules, JSON error handlers, CLI commands, and pytest fixtures with test_client. Use when starting a Flask project, refactoring a single-file app.py into blueprints, fixing "working outside of application context" errors, adding extensions, or preparing Flask for gunicorn in production.

Use it when

  • Starting a Flask project or splitting a large app.py
  • Adding extensions, blueprints, CLI commands or JSON error handling
  • Context errors appear in scripts, threads or tests
  • Preparing a Flask app for gunicorn behind a proxy

Use something else when

  • You are starting a new async-first JSON API: use fastapi-services
  • The question is SQLAlchemy query design rather than Flask wiring: use sqlalchemy-2

How to check the result

pytest passes using create_app with test config, flask routes lists every blueprint under its prefix, and an unknown URL returns the JSON 404.

The mistake it prevents

Importing a module-level app object across files, which causes circular imports and makes tests share one configured instance.

~/.agents/skills/flask-apps/SKILL.md

--- name: flask-apps description: Structures Flask 3 applications with the app factory pattern - create_app with config objects and prefixed environment variables, blueprints per… license: MIT ---

Done well means: the app is built by create_app() so tests and CLI get fresh, configurable instances; features live in blueprints; extensions are created once at module level and bound with init_app; no code relies on a global app import; and configuration comes from the environment with safe defaults only for development.

Inspect first

  • Read AGENTS.md, then pyproject.toml/requirements.txt for Flask (3.x assumed), extensions (Flask-SQLAlchemy, Flask-Migrate, Flask-Login, Flask-WTF) and the test runner.
  • Find how the app is created and run: rg -n "Flask\(__name__|create_app|FLASK_APP|--app" ..
  • Find imports of a global app: rg -n "from .* import app\b" .; these are the circular-import and testing problems the factory removes.

Layout

src/myapp/
  __init__.py          # create_app()
  config.py            # Config, TestConfig
  extensions.py        # db = SQLAlchemy(...), migrate = Migrate(), login = LoginManager()
  models.py            # or per-blueprint models
  errors.py            # register_error_handlers(app)
  orders/
    __init__.py        # bp = Blueprint("orders", __name__)
    routes.py
    service.py
tests/
  conftest.py
migrations/            # Flask-Migrate (Alembic)

The factory

# extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from sqlalchemy.orm import DeclarativeBase

class Base(DeclarativeBase):
    pass

db = SQLAlchemy(model_class=Base)
migrate = Migrate()
# __init__.py
from flask import Flask

def create_app(config: dict | None = None) -> Flask:
    app = Flask(__name__)
    app.config.from_object("myapp.config.Config")
    app.config.from_prefixed_env()          # FLASK_SECRET_KEY -> SECRET_KEY, values parsed as JSON when possible
    if config:
        app.config.update(config)            # tests override here

    if not app.config.get("SECRET_KEY"):
        raise RuntimeError("SECRET_KEY is not set")

    from .extensions import db, migrate
    db.init_app(app)
    migrate.init_app(app, db)

    from .orders import bp as orders_bp
    app.register_blueprint(orders_bp, url_prefix="/orders")

    from .errors import register_error_handlers
    register_error_handlers(app)

    from . import cli
    cli.register(app)
    return app
# config.py
class Config:
    SQLALCHEMY_DATABASE_URI = "sqlite:///dev.db"     # overridden by FLASK_SQLALCHEMY_DATABASE_URI
    SQLALCHEMY_ENGINE_OPTIONS = {"pool_pre_ping": True}
    MAX_CONTENT_LENGTH = 10 * 1024 * 1024
    SESSION_COOKIE_SECURE = True
    SESSION_COOKIE_HTTPONLY = True
    SESSION_COOKIE_SAMESITE = "Lax"

Import blueprints inside the factory to avoid circular imports between blueprints and extensions.

Blueprints and routes

# orders/__init__.py
from flask import Blueprint
bp = Blueprint("orders", __name__)
from . import routes  # noqa: E402,F401  (registers routes on bp)
# orders/routes.py
from flask import abort, jsonify, request
from flask_login import current_user, login_required
from ..extensions import db
from ..models import Order
from . import bp

@bp.post("")
@login_required
def create_order():
    data = request.get_json(silent=True) or {}
    qty = data.get("qty")
    if not isinstance(qty, int) or qty < 1:
        abort(422, description="qty must be a positive integer")
    order = Order(customer_id=current_user.id, sku=str(data.get("sku", "")), qty=qty)
    db.session.add(order)
    db.session.commit()
    return jsonify(id=order.id, sku=order.sku, qty=order.qty), 201

@bp.get("/<int:order_id>")
@login_required
def get_order(order_id: int):
    order = db.first_or_404(db.select(Order).filter_by(id=order_id, customer_id=current_user.id))
    return jsonify(id=order.id, sku=order.sku, qty=order.qty)
  • Use the <int:...> converters so bad ids 404 before reaching code.
  • For anything more than a couple of fields, validate with a schema library already in the project (Pydantic, marshmallow) rather than hand checks.
  • Scope lookups by owner, as above; db.get_or_404(Order, id) alone is an IDOR on user-owned data.
  • Flask-SQLAlchemy 3 prefers db.session.execute(db.select(...)) and db.get_or_404; the legacy Model.query interface still works but do not add new uses.

Contexts

ErrorCauseFix
“Working outside of application context”Using current_app, db.session or url_for in a script, thread or import-time codewith app.app_context(): or move the code into a request/CLI command
“Working outside of request context”Using request, session outside a requestPass values in as arguments; in tests use app.test_request_context()
Data from one request seen in anotherStoring per-request state in module globalsUse g (per app context) or pass explicitly

Background threads do not inherit contexts. Capture the values you need, or push app.app_context() inside the thread with the app object passed in. Prefer a real job queue: see $background-jobs.

Errors

from flask import jsonify
from werkzeug.exceptions import HTTPException

def register_error_handlers(app):
    @app.errorhandler(HTTPException)
    def http_error(e: HTTPException):
        return jsonify(error={"code": e.code, "name": e.name, "message": e.description}), e.code

    @app.errorhandler(Exception)
    def unhandled(e: Exception):
        app.logger.exception("unhandled error")
        return jsonify(error={"code": 500, "message": "Internal server error"}), 500

Blueprint-level @bp.errorhandler only handles errors raised in that blueprint’s views, and does not handle 404s for unmatched URLs (those never reach a blueprint).

CLI commands

# cli.py
import click

def register(app):
    @app.cli.command("seed")
    @click.option("--count", default=10)
    def seed(count: int):
        """Insert demo orders (development only)."""
        ...

Commands registered on app.cli run inside an app context automatically. Run with flask --app myapp seed --count 5.

Migrations

flask --app myapp db migrate -m "add orders"
# review migrations/versions/<rev>_add_orders.py before applying
flask --app myapp db upgrade

Alembic autogenerate misses some changes (renames, server defaults, some constraint types); read every script.

Testing

# tests/conftest.py
import pytest
from myapp import create_app
from myapp.extensions import db

@pytest.fixture
def app():
    app = create_app({"TESTING": True, "SECRET_KEY": "test",
                      "SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:", "WTF_CSRF_ENABLED": False})
    with app.app_context():
        db.create_all()
        yield app
        db.drop_all()

@pytest.fixture
def client(app):
    return app.test_client()

Test with the production database engine in CI if SQL differs (JSON columns, constraints); SQLite in-memory is fine for logic-only tests. app.test_cli_runner().invoke(args=[...]) tests CLI commands.

Production

  • gunicorn "myapp:create_app()" --workers 3 --bind 0.0.0.0:8000. Never flask run or debug=True in production; the debugger allows code execution.
  • Behind a proxy, wrap with werkzeug.middleware.proxy_fix.ProxyFix(app.wsgi_app, x_for=1, x_proto=1) using the real hop count.
  • Flask 3.1 adds SECRET_KEY_FALLBACKS for key rotation and MAX_FORM_MEMORY_SIZE / MAX_FORM_PARTS limits; set MAX_CONTENT_LENGTH regardless.
  • Async views work but each request still occupies a WSGI worker; for async-first services consider Quart or FastAPI ($fastapi-services).

Verification

pytest -q
flask --app myapp routes          # every blueprint route registered with the intended prefix
flask --app myapp db check        # Flask-Migrate: fails if models and migrations differ (needs a DB)

Then run the app locally and request one route per blueprint plus an unknown URL (expect the JSON 404, not an HTML page).

Gotchas

  • app.config.from_prefixed_env() must run after from_object or the defaults overwrite environment values.
  • request.get_json() raises 415 when the Content-Type is not JSON; use silent=True and validate.
  • jsonify of SQLAlchemy objects fails; serialize explicitly or use a schema.
  • Flask-Login’s login_required redirects API clients to the HTML login page whenever login_manager.login_view is set; register an unauthorized_handler that returns 401 JSON for API blueprints.

Output template

## Flask change: <summary>
- Factory/config: <changes, new env vars>
- Blueprints: <name, prefix, routes>
- Extensions: <added, init_app placement>
- Migrations: <revision, reviewed>
- Tests: <fixtures, cases>

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Flask Apps"
  short_description: "From one app.py to a testable Flask factory"
  default_prompt: "$flask-apps Refactor our single-file Flask app into an app factory with blueprints"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem