Skip to main content

๐Ÿงฉ 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.

flowchart TD APP["Flask app (create_app)"] --> M[main blueprint] APP --> A[auth blueprint] APP --> B[blog blueprint] APP --> API[api blueprint] M --> M1["/ ยท /about"] A --> A1["/login ยท /register"] B --> B1["/posts ยท /post/<id>"] API --> API1["/api/v1/..."]

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:

  1. Create three blueprints: main (/), auth (prefix /auth, route /login), blog (prefix /blog, route /posts).
  2. Move each route to its blueprint, swapping @app.route for @bp.route.
  3. Write create_app() that registers all three.
  4. Update any template link from url_for('login') to url_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 errors blueprint via app_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.
  • errorhandler is blueprint-scoped; app_errorhandler is 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

๐Ÿš€ 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.