Splitting a Monolithic Flask App with Blueprints
Flask Blueprints let you split a monolithic app into modules with URL prefixes, scoped error handlers, and namespaced URL generation — without changing route logic. A worked example shows API versioning via url_prefix, plus the app_errorhandler vs errorhandler distinction that trips people up.
30 Jul 2026, 17:47 UTC

A Flask app that starts as one app.py tends to grow quietly: routes for the web UI, routes for an API, admin pages, and a few utility endpoints all end up in a single file. The problem is not that Flask can't handle this — it's that you can't, six months later. Blueprints are Flask's built-in answer: a blueprint groups related routes, templates, and static files into a module that you register on the app, with URL prefixes and scoped error handling. The takeaway: you can split a monolithic app into modules without changing your routes' logic, and the migration is incremental — you can move one section at a time.
What a Blueprint Actually Is
A blueprint is a deferred registration pattern. You create a Blueprint object, decorate functions with @bp.route(...), but nothing is live yet. Only when you call app.register_blueprint(bp) do the routes become part of the application. This separation is what enables the modular structure: the blueprint module never needs to import the app object, which also eliminates the circular import problem that plagues factory-pattern Flask apps.
Because blueprints are not applications, they can't run standalone. This is a common point of confusion: a blueprint with no register_blueprint call is dead code. If your app seems to ignore a module, check registration first.
A Worked Example: API Versioning with URL Prefixes
Suppose you want a versioned API. Create api_v1.py:
from flask import Blueprint
bp = Blueprint('api_v1', __name__)
@bp.route('/users')
def list_users():
return {'users': []}Then in your app factory:
from flask import Flask
from api_v1 import bp as api_v1
def create_app():
app = Flask(__name__)
app.register_blueprint(api_v1, url_prefix='/api/v1')
return appThe url_prefix argument prepends /api/v1 to every route in the blueprint, so /users becomes /api/v1/users. When v2 arrives, you create a second blueprint with the same logic and register it under /api/v2 — no duplication of route functions, and the versioning is explicit in the registration call rather than scattered across decorators.
To verify the routes landed where you expect, run flask routes (Flask CLI, requires the app to be importable via FLASK_APP) and confirm the prefixed paths. For URL generation, url_for('api_v1.list_users') resolves to /api/v1/users. The blueprint_name.function_name form is the key detail: endpoint names are namespaced by the blueprint, which prevents collisions between modules that happen to have functions with the same name.
Scoped Error Handlers and Template Context
Error handlers registered on a blueprint with @bp.app_errorhandler(404) apply to the whole app, not just the blueprint — the naming is misleading. If you want blueprint-local errors, use @bp.errorhandler(404) (without app_), and be aware that blueprint-scoped 404 handlers only fire for URLs within that blueprint's territory. For 500 errors, app-level handlers are usually what you want; per-blueprint 500 handlers are easy to over-engineer.
Similarly, @bp.app_context_processor applies globally, while @bp.context_processor is blueprint-scoped. Mixing these up is a common source of "works on my machine" bugs in template context.
Limitations and Trade-offs
Blueprints add indirection. URL generation requires the blueprint name, so refactoring a function from one blueprint to another changes every url_for call that references it. Also, url_prefix is fixed at registration time — you can't have one blueprint serve under two prefixes without registering it twice, and registering the same blueprint twice creates duplicate endpoint names. For a small app, this overhead isn't worth it; the crossover point is roughly when your single file exceeds what you can hold in your head.
Where to Start
Pick the most cohesive cluster of routes — usually the API or the admin section — and move it into a blueprint. Run flask routes before and after to confirm nothing shifted. If the app still passes its tests, move the next cluster. The app factory stays thin: create the app, register blueprints, done.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.