Blueprints and the application factory

Split a growing app into blueprints, build the app inside create_app() for testability, and initialise extensions without circular imports.

Blueprints

A blueprint is a collection of routes, templates and handlers that is registered on an application later. It is not an application: it has no config, no request handling of its own, and it cannot be run. That distinction is what makes it useful β€” the same blueprint can be mounted on a test app, mounted twice with different prefixes, or not mounted at all.

# app/shop/views.py
from flask import Blueprint, render_template, abort
from .models import Product

bp = Blueprint(
    "shop", __name__,
    url_prefix="/shop",
    template_folder="templates",     # app/shop/templates/shop/detail.html
    static_folder="static",          # served under /shop/static
)

@bp.get("/")
def index():
    return render_template("shop/index.html", products=Product.all())

@bp.get("/<int:product_id>")
def detail(product_id):
    product = Product.get(product_id)
    if product is None:
        abort(404)
    return render_template("shop/detail.html", product=product)

@bp.before_request
def require_open_shop():
    if not current_app.config["SHOP_OPEN"] and request.endpoint != "shop.index":
        return redirect(url_for("shop.index"))
Blueprint featureBehaviour
url_prefixPrepended to every rule in the blueprint
Endpoint nameshop.detail β€” the blueprint name, a dot, then the function name
template_folderAdded to the template search path, not a namespace
static_folderServed under <url_prefix>/static when a prefix is set
before_request / after_requestRun only for requests that match this blueprint
errorhandlerCatches errors raised inside this blueprint's routes
Nested blueprintsA blueprint registered on a blueprint, for deep prefixes

The application factory

# app/__init__.py
from flask import Flask
from .extensions import db, migrate, login_manager

def create_app(config_object="app.config.DevelopmentConfig"):
    app = Flask(__name__, instance_relative_config=True)
    app.config.from_object(config_object)

    db.init_app(app)
    migrate.init_app(app, db)
    login_manager.init_app(app)

    from .shop.views import bp as shop_bp
    from .blog.views import bp as blog_bp
    app.register_blueprint(shop_bp)          # /shop/...
    app.register_blueprint(blog_bp, url_prefix="/blog")

    @app.get("/healthz")
    def healthz():
        return {"ok": True}

    return app

# run.py (development)
from app import create_app
app = create_app()
if __name__ == "__main__":
    app.run(debug=True)

# production β€” gunicorn resolves the factory for you
# gunicorn "app:create_app()"
  • The import of the blueprint modules happens inside the factory. That is what breaks the circular dependency: views import db from the extension module, and the extension module never imports the app.
  • Everything that needs an app object happens after create_app() is called β€” configuration reads, extension registration, CLI commands, error handlers. Module-level code that touches current_app fails at import.
  • instance_relative_config=True lets app.config.from_pyfile("config.py", silent=True) read a file outside version control, which is where machine-specific settings belong.
  • Tests build a fresh app with the testing config. Two independent apps in one process only works because nothing is global.

Extensions, contexts and CLI commands

# app/extensions.py β€” one module, no app import
from flask_login import LoginManager
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()
login_manager.login_view = "auth.login"

# scripts and CLI work: you need an application context
from app import create_app
application = create_app()
with application.app_context():
    print(db.session.execute(db.select(db.func.now())).scalar())

# register a command on the app, from anywhere in the package
import click

@app.cli.command("seed")
@click.option("--count", default=10)
def seed(count):
    """Create demo rows."""
    create_demo_data(count)
    click.echo(f"seeded {count} rows")

# shell context: names preloaded in "flask shell"
@app.shell_context_processor
def shell_context():
    return {"db": db, "User": User}
  • An extension object is created once and bound to an app with init_app. Calling init_app twice on the same app raises; creating two apps in one process is exactly what the pattern is for.
  • The app context is not optional. current_app, g, url_for without a request, and any database query all need one β€” inside a view Flask pushes it for you, in a script you push it yourself.
  • Blueprint-scoped CLI commands are registered by the blueprint's own @bp.cli.command and appear once the blueprint is registered.
⚠️
The two failure modes of this pattern are opposite and equally confusing. Module-level work β€” a query, a current_app.config read, an app.route call β€” raises RuntimeError: Working outside of application context the moment the package is imported, before any request exists. The reverse mistake is creating the app at import time in a module that views also import, which produces a circular import or two Flask apps living in one process, each with its own extension registrations. Keep exactly one create_app() call site: the WSGI entry point, or a fixture in tests.

FAQ

Blueprint or just more modules?
A blueprint when the group owns routes, templates and handlers that belong together and can be mounted at a prefix. A plain module of helper functions needs no blueprint β€” blueprints are for the HTTP surface.
How do I reuse one blueprint under two prefixes?
Register it twice with different name= and url_prefix= values, for example app.register_blueprint(bp, name="admin_shop", url_prefix="/admin/shop"). Endpoint names then differ too, so url_for resolves to the right mount.

Configuration and project structure Databases with Flask-SQLAlchemy

Last refreshed 2026-09-18.