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:
| Blueprint | url_prefix | Owns |
|---|---|---|
books | /books | Listing, viewing, creating, editing, and deleting books |
shelves | /shelves | Creating shelves and viewing the books on each one |
auth | /auth | Registration, 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.
g, current_app, and the application context actually manage behind the scenes.