Blueprints

Bookshelf now has routes for books, and by the time shelves, tags, and user accounts each get their own views too, a single app.py holding every route is heading toward being unmanageable. This lesson covers Blueprints, Flask's built-in answer to organizing a growing app into coherent, separate pieces.

Why One Big routes.py Stops Working

A small Flask app with five or six routes in one file is genuinely easy to work with — everything is one Ctrl+F away. Bookshelf, though, is heading toward routes for books, shelves, tags, user registration and login, and more, each with several view functions of its own. Cram all of that into one file and a few real problems show up: unrelated features sit tangled together, merge conflicts multiply as several people edit the same giant file, and finding "the shelf routes" means scrolling past everything else first.

What a Blueprint Is

A Blueprint is Flask's way of grouping a set of related routes, templates, and static files into one self-contained, reusable component — essentially a mini Flask app that gets plugged into the real one. Instead of decorating view functions with @app.route(), a blueprint's routes are decorated with @bp.route(), where bp is that blueprint's own object:

Example: A books Blueprint

# books/routes.py
from flask import Blueprint, render_template, redirect, url_for
from extensions import db
from models import Book

bp = Blueprint('books', __name__)


@bp.route('/')
def book_list():
    books = Book.query.all()
    return render_template('books/list.html', books=books)


@bp.route('/<int:book_id>')
def book_detail(book_id):
    book = Book.query.get_or_404(book_id)
    return render_template('books/detail.html', book=book)

Blueprint('books', __name__) creates the blueprint with the name 'books' — that name matters later, since it becomes the first half of every one of this blueprint's endpoint names. Everything else about writing a route inside a blueprint looks exactly like a normal Flask view; the only real change is swapping @app.route for @bp.route.

Registering a Blueprint with url_prefix

A blueprint on its own does nothing — it has to be registered onto the real Flask app, which happens inside the app factory alongside initializing Flask-SQLAlchemy and Flask-Migrate:

# app.py
from flask import Flask
from extensions import db, migrate
from books.routes import bp as books_bp

def create_app():
    app = Flask(__name__)
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///bookshelf.db'

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

    app.register_blueprint(books_bp, url_prefix='/books')

    return app

url_prefix='/books' is what automatically prepends /books onto every route declared inside that blueprint — so @bp.route('/') inside books/routes.py actually answers at /books/, and @bp.route('/<int:book_id>') answers at /books/5. The blueprint's own route definitions never need to know their final prefix at all; that decision lives entirely at registration time in the app factory.

A Realistic Bookshelf Blueprint Breakdown

Applied across the whole app, Bookshelf naturally splits into a handful of blueprints, each owning one feature area:

Blueprinturl_prefixOwns
books/booksListing, viewing, creating, editing, and deleting books
shelves/shelvesCreating shelves and viewing the books on each one
auth/authRegistration, login, and logout

Each of those can live in its own subfolder — books/routes.py, shelves/routes.py, auth/routes.py — and, when a feature's pages look different enough to warrant it, its own templates/ subfolder as well, keeping everything about "books" (routes, and optionally its templates) in one place instead of scattered across shared top-level folders.

Referencing Routes by Namespaced Endpoint

Once routes live inside blueprints, url_for() needs the blueprint's name as part of the endpoint, using blueprint_name.view_function_name:

url_for('books.book_detail', book_id=5)

This trips people up almost every time they first split a single-file app into blueprints: a call that used to be url_for('book_detail', book_id=5) now needs the 'books.' prefix, because book_detail alone is no longer a unique endpoint name once several blueprints could each define a view function with that same name. 'books.book_detail' unambiguously means "the book_detail view inside the books blueprint" — and this same dotted syntax applies both in Python view code and inside Jinja2 templates:

<a href="{{ url_for('books.book_detail', book_id=book.id) }}">{{ book.title }}</a>

Combining a Couple of Blueprints Together

Here's create_app() with both the books and shelves blueprints registered side by side:

# app.py
from flask import Flask
from extensions import db, migrate
from books.routes import bp as books_bp
from shelves.routes import bp as shelves_bp

def create_app():
    app = Flask(__name__)
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///bookshelf.db'

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

    app.register_blueprint(books_bp, url_prefix='/books')
    app.register_blueprint(shelves_bp, url_prefix='/shelves')

    return app

Each blueprint import is aliased to bp inside its own module and renamed on the way in (as books_bp, as shelves_bp) purely so both can be registered in the same file without their names colliding — the blueprint's actual name, the one url_for() uses, still comes from the string passed into Blueprint('books', __name__) and Blueprint('shelves', __name__) respectively, not from whatever the import is aliased to.

An honest note on when to bother: blueprints are worth reaching for once an app has grown past a handful of routes covering more than one feature area — exactly the point Bookshelf is at now. A small single-file app with a few routes doesn't need this structure yet, and forcing it in early just adds ceremony (extra files, extra imports, the dotted endpoint syntax) for no real organizational benefit.
Next up: Head to Application Context & Globals to see what Flask's g, current_app, and the application context actually manage behind the scenes.