๐งฉ Blueprints for Modular Applications
One giant app.py with a hundred routes is a maintenance nightmare. Flask blueprints let you carve an application into self-contained feature modules โ auth here, blog there, an API over there โ that snap into the app factory like Lego bricks. This lesson shows you how to design, register, and structure them.
๐ฏ Learning Objectives
By the end of this lesson, you will be able to:
- Create a blueprint and register it with an application factory
- Control routing with URL prefixes, subdomains, and nested blueprints
- Generate correct links across modules using
url_for('blueprint.view') - Attach blueprint-specific templates, hooks, and error handlers
- Choose between functional and divisional project structures
Estimated Time: 35โ45 minutes โข Difficulty: Intermediate
Hands-on: Split a single-file app into three blueprints and wire them into a factory.
In This Lesson
What Is a Blueprint?
A blueprint is a reusable collection of routes, templates, static files, and hooks that you define separately from the application and then register onto an app. It's Flask's built-in answer to the "separation of concerns" problem: instead of every view decorated with @app.route in one file, each feature area owns its own module.
๐ก Analogy: Think of an architect's plans for a building. There's a separate blueprint for plumbing, one for electrical, one for the structure. Each is drawn on its own, but they all come together into a single building. A Flask blueprint is exactly that โ a self-contained plan for one part of your app that gets assembled into the whole at registration time.
Crucially, a blueprint is not an app. It records what you want (routes, handlers) as a set of deferred operations, and only when you call app.register_blueprint() does Flask replay those operations onto the real application.
Creating & Registering
Creating a blueprint looks almost identical to creating an app, except you decorate views with the blueprint object instead of app.
# app/blueprints/main.py
from flask import Blueprint, render_template
# name -> used by url_for(); __name__ -> helps locate templates/static
main = Blueprint("main", __name__)
@main.route("/")
def index():
return render_template("main/index.html")
@main.route("/about")
def about():
return render_template("main/about.html")
The blueprint does nothing until it's registered onto an app โ which, in a factory-based project, happens inside create_app():
# app/__init__.py
from flask import Flask
def create_app():
app = Flask(__name__)
from app.blueprints.main import main
app.register_blueprint(main)
return app
๐ The two names to keep straight
Blueprint name (first argument, "main"): the prefix used in url_for("main.index"). Must be unique across the app.
Import name (__name__): tells Flask where the blueprint's package lives so it can find relative templates/ and static/ folders.
You can pass options at creation time, at registration time, or both:
# Prefix set when creating the blueprint...
auth = Blueprint("auth", __name__, url_prefix="/auth")
# ...or when registering it (registration wins if both are set)
app.register_blueprint(auth, url_prefix="/account")
URL Prefixes & Nesting
Prefixes
A url_prefix is prepended to every route in the blueprint. With url_prefix="/auth", a view mapped to /login is served at /auth/login. This keeps each feature's URLs namespaced and collision-free.
auth = Blueprint("auth", __name__, url_prefix="/auth")
@auth.route("/login") # -> /auth/login
def login():
return render_template("auth/login.html")
@auth.route("/register") # -> /auth/register
def register():
return render_template("auth/register.html")
Nested blueprints
Modern Flask (1.2+) supports registering a blueprint onto another blueprint, which is perfect for versioned APIs. Prefixes compose from outer to inner:
# app/blueprints/api/v1.py
from flask import Blueprint, jsonify
api_v1 = Blueprint("v1", __name__, url_prefix="/v1")
@api_v1.route("/users")
def users():
return jsonify(users=[])
# app/blueprints/api/__init__.py
from flask import Blueprint
from app.blueprints.api.v1 import api_v1
api = Blueprint("api", __name__, url_prefix="/api")
api.register_blueprint(api_v1) # nesting: /api + /v1 + /users -> /api/v1/users
๐ก Subdomains too
Blueprints can bind to a subdomain instead of (or as well as) a path prefix. Set subdomain="admin" on the blueprint and SERVER_NAME in config, and its routes answer at admin.yourdomain.com. Handy for admin panels and multi-tenant apps.
Linking with url_for
Because routes now live in namespaces, you reference them by blueprint_name.view_name. This is the single most important habit to build with blueprints โ hard-coded URLs break the moment you change a prefix; url_for never does.
from flask import url_for
# Same blueprint
url_for("blog.show_post", post_id=42) # -> /blog/post/42
# Another blueprint
url_for("auth.login") # -> /auth/login
url_for("main.index") # -> /
Inside templates the syntax is identical:
<a href="{{ url_for('auth.login') }}">Log in</a>
<a href="{{ url_for('blog.show_post', post_id=post.id) }}">{{ post.title }}</a>
โ ๏ธ A common gotcha
If you write url_for("login") after moving login into the auth blueprint, Flask raises BuildError. The endpoint is now auth.login. Within the same blueprint you can use a leading dot as a shortcut: url_for(".login") resolves to the current blueprint.
Templates, Hooks & Errors
Blueprint templates
A blueprint can carry its own templates/ folder. Flask searches the app's template folder first, then each blueprint's. Namespacing templates in a subfolder (auth/login.html) avoids clashes between blueprints that both have an index.html.
admin = Blueprint("admin", __name__, template_folder="templates")
@admin.route("/users")
def users():
# looks for templates/admin/users.html inside the blueprint,
# then falls back to the app's template folder
return render_template("admin/users.html")
Request hooks
Blueprints can run code before or after each of their requests โ ideal for scoping a permission check to one area:
from flask import g, abort
@admin.before_request
def require_admin():
"""Runs before every admin route only."""
if g.user is None or not g.user.is_admin:
abort(403)
@admin.after_request
def add_header(response):
response.headers["X-Admin"] = "1"
return response
Error handlers: scoped vs. app-wide
This distinction trips up a lot of people. @bp.errorhandler handles errors raised inside that blueprint's routes. @bp.app_errorhandler registers an application-wide handler from within a blueprint โ useful for putting your global 404/500 pages in a dedicated errors blueprint.
errors = Blueprint("errors", __name__)
@errors.app_errorhandler(404) # applies to the WHOLE app
def page_not_found(error):
return render_template("errors/404.html"), 404
@errors.app_errorhandler(500)
def server_error(error):
return render_template("errors/500.html"), 500
โ Rule of thumb
Want it to apply only to this feature? Use errorhandler. Want it to cover the entire app but keep the code modular? Use app_errorhandler. The same app_ prefix applies to app_template_filter and app_context_processor.
Structuring a Real App
As projects grow you'll pick between two organizing philosophies.
| Functional (by type) | Divisional (by feature) | |
|---|---|---|
| Groups by | Layer: all views together, all models together, all forms together | Feature: everything for "blog" together, everything for "shop" together |
| Best for | Small apps, or teams organized by role | Larger apps; keeps related code together and easy to reason about |
| Trade-off | One feature's code is scattered across folders | Slightly more boilerplate per feature |
For most real applications, divisional wins โ each blueprint becomes a mini-package that owns its routes, models, and forms:
app/
โโโ __init__.py # create_app()
โโโ extensions.py
โโโ config.py
โโโ blueprints/
โโโ main/
โ โโโ __init__.py # Blueprint + `from . import routes`
โ โโโ routes.py
โโโ auth/
โ โโโ __init__.py
โ โโโ routes.py
โ โโโ models.py
โ โโโ forms.py
โโโ blog/
โโโ __init__.py
โโโ routes.py
โโโ models.py
โโโ forms.py
The lazy-import pattern in each package's __init__.py avoids circular imports: create the blueprint first, then import the routes that decorate it.
# app/blueprints/blog/__init__.py
from flask import Blueprint
blog = Blueprint("blog", __name__)
# Import routes AFTER the blueprint exists so the decorators can attach
from app.blueprints.blog import routes # noqa: E402,F401
# app/blueprints/blog/routes.py
from flask import render_template
from app.blueprints.blog import blog
from app.blueprints.blog.models import Post
@blog.route("/posts")
def list_posts():
posts = Post.query.order_by(Post.created_at.desc()).all()
return render_template("blog/posts.html", posts=posts)
Hands-on Exercise
๐๏ธ Break Up a Monolith
Objective: Convert a single-file app into three blueprints registered by a factory.
Starting point โ one file with everything:
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/")
def index(): return render_template("index.html")
@app.route("/login")
def login(): return render_template("login.html")
@app.route("/posts")
def posts(): return render_template("posts.html")
Instructions:
- Create three blueprints:
main(/),auth(prefix/auth, route/login),blog(prefix/blog, route/posts). - Move each route to its blueprint, swapping
@app.routefor@bp.route. - Write
create_app()that registers all three. - Update any template link from
url_for('login')tourl_for('auth.login').
๐ก Hint
The login route becomes /auth/login once you add the prefix โ that's expected. Its endpoint name is now auth.login, so every url_for that referenced login must be updated.
โ Solution
# app/blueprints/main.py
from flask import Blueprint, render_template
main = Blueprint("main", __name__)
@main.route("/")
def index():
return render_template("index.html")
# app/blueprints/auth.py
from flask import Blueprint, render_template
auth = Blueprint("auth", __name__, url_prefix="/auth")
@auth.route("/login")
def login():
return render_template("login.html")
# app/blueprints/blog.py
from flask import Blueprint, render_template
blog = Blueprint("blog", __name__, url_prefix="/blog")
@blog.route("/posts")
def posts():
return render_template("posts.html")
# app/__init__.py
from flask import Flask
def create_app():
app = Flask(__name__)
from app.blueprints.main import main
from app.blueprints.auth import auth
from app.blueprints.blog import blog
app.register_blueprint(main)
app.register_blueprint(auth)
app.register_blueprint(blog)
return app
Best Practices
โ Do
- Give each blueprint one clear responsibility โ auth, blog, admin, api.
- Namespace templates in a matching subfolder (
auth/login.html). - Always use
url_for('bp.view')instead of hard-coded paths. - Import routes after creating the blueprint to dodge circular imports.
- Put global error pages in an
errorsblueprint viaapp_errorhandler.
โ ๏ธ Don't
- Don't let a blueprint balloon into a second monolith โ split it when it grows.
- Don't create tight coupling between blueprints; prefer signals or a shared service layer.
- Don't reuse blueprint names โ each must be unique, or registration fails.
- Don't hard-code URLs in templates; they break when prefixes change.
Summary & Quiz
๐ Key Takeaways
- A blueprint bundles routes, templates, and hooks into a reusable module registered onto an app.
- URL prefixes namespace routes; nested blueprints compose prefixes (great for
/api/v1). - Reference views with
url_for("blueprint.view")โ never hard-coded paths. errorhandleris blueprint-scoped;app_errorhandleris app-wide from within a blueprint.- Divisional structure (by feature) scales better than functional for large apps.
๐ฏ Quick Quiz
Question 1: A blueprint auth = Blueprint("auth", __name__, url_prefix="/auth") has a view mapped to /login. What URL serves it?
Question 2: How do you build a link to the login view of the auth blueprint?
Question 3: You want a 404 page that applies to the entire app, but you want to define it inside an errors blueprint. Which decorator do you use?
๐ Further Reading
- Flask Docs โ Modular Applications with Blueprints
- Flask Tutorial โ Blueprints and Views
- Real Python โ Use a Flask Blueprint to Architect Your Applications
๐ What's Next?
Your factory and blueprints both need settings โ database URLs, secret keys, per-environment toggles. Next: Configuration Management, where you'll learn to load config cleanly and keep secrets out of your repo.
๐ Nicely modular!
You can now grow a Flask app to dozens of features without it collapsing into one unreadable file.