Blueprints

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Flask 3.1.3 (released 2026-02-18), Werkzeug 3.1.9 and Click 8.5.0, checked on 2026-10-07, and every code block was executed on Python 3.13. Flask 3.2 is not released yet; the changes it lists that touch contexts are described as upcoming at the end of the page. A blueprint is how Flask splits one application into components: a Blueprint object records routes, error handlers, template and static folders, hooks and commands without needing an application, and register_blueprint replays the recording onto a real application, optionally under a URL prefix, on a subdomain or under a different name. This page follows the Bookshelf project of this section from a single Blueprint(...) line to a tree of nested, prefixed and twice-registered blueprints, and explains where a blueprint stops being the right tool.

Why Blueprints

The official documentation lists the cases blueprints are meant for:

  • Factor an application into a set of blueprints. A project instantiates one application object, initialises several extensions and registers a collection of blueprints. This is the shape of the Bookshelf factory on Application Factory & Project Layout.

  • Register a blueprint at a URL prefix and/or on a subdomain. Parameters in the prefix or subdomain become common view arguments across every view of the blueprint.

  • Register a blueprint several times on one application with different URL rules.

  • Provide template filters, static files, templates and other utilities. A blueprint does not have to implement views.

  • Register a blueprint on an application when an extension is initialised. Flask-Admin, flask-smorest and other extensions ship blueprints or blueprint subclasses that you register like your own.

Three properties make a blueprint different from "a small Flask app":

  • It is not an application. It is a set of recorded operations. There is no WSGI callable, no configuration of its own, no request or application context of its own.

  • It shares the application’s configuration, the single URL map, the extensions and the one app.config, so a blueprint reads settings through current_app.config and talks to extensions the same way the rest of the code does.

  • It cannot be unregistered. Once registered, a blueprint stays until the application object is destroyed. Test suites build a fresh application per test through the factory instead of removing blueprints.

The alternative is several real application objects glued together at the WSGI layer (see Blueprints Versus Separate Applications); that gives full isolation, separate configurations and separate deployments at the price of everything being shared explicitly.

What a blueprint is not is a reusable "Django app" with its own models and migrations. Flask has no such unit; a blueprint is only the web-layer glue, and a feature that owns models also owns a package that contains the blueprint (see A Blueprint as a Package: Views, Models and Forms Together).

The Bookshelf Blueprint Tree

Bookshelf ends this section with seven blueprints registered on one application. Each adds a URL prefix and an endpoint prefix; the page builds each of them in the sections that follow.

The Bookshelf application at the top with a tree of blueprints below it: core with no prefix, catalog at /books, auth at /auth, admin at /admin with its own templates and static folder, api at /api with a nested v1 blueprint at /v1, and pages registered twice at /en and /es
The Bookshelf blueprint tree: URL prefix and endpoint prefix per blueprint

The endpoint prefix is the other half of the contribution: Flask names a blueprint endpoint <registered name>.<view function name> and a nested one <parent>.<child>.<view>. URLs are what browsers see; endpoints are what url_for, request.endpoint and the before_request machinery use. The two are independent: the blueprint name never changes the URL, and the URL prefix never changes the endpoint.

A First Blueprint

from flask import Blueprint, Flask, abort, jsonify, url_for

BOOKS = {
    "9780132350884": {"title": "Clean Code", "author": "Robert C. Martin"},
    "9780134494166": {"title": "Clean Architecture", "author": "Robert C. Martin"},
}

catalog = Blueprint("catalog", __name__, url_prefix="/books")


@catalog.get("/")
def list_books():
    return jsonify([{"isbn": isbn, **book} for isbn, book in BOOKS.items()])


@catalog.get("/<isbn>")
def show_book(isbn):
    book = BOOKS.get(isbn)
    if book is None:
        abort(404)
    return jsonify({"isbn": isbn, **book})


app = Flask("bookshelf")
app.register_blueprint(catalog)

for rule in sorted(app.url_map.iter_rules(), key=lambda r: r.rule):
    print(f"{rule.rule:22} -> {rule.endpoint}")

client = app.test_client()
assert client.get("/books/").get_json()[0]["title"] == "Clean Code"
assert client.get("/books/9780134494166").get_json()["title"] == "Clean Architecture"
assert client.get("/books/0000000000000").status_code == 404

with app.test_request_context():
    assert url_for("catalog.show_book", isbn="9780132350884") == "/books/9780132350884"

Printing the URL map shows the two halves of the contribution:

/books/                -> catalog.list_books
/books/<isbn>          -> catalog.show_book
/static/<path:filename> -> static

Blueprint offers the same registration decorators as Flask: route, the HTTP-method shortcuts get, post, put, delete and patch, and add_url_rule. When a function is decorated, the blueprint does not touch any application; it records the intention (see What Happens When a Blueprint Is Registered). The first argument to Blueprint is the name that prefixes every endpoint of the blueprint. It must be non-empty (an empty name raises ValueError since 2.3) and must not contain a dot, because the dot separates blueprint names and endpoint names (ValueError since 2.0.1). A view function name cannot contain a dot either.

The Blueprint Constructor

from flask import Blueprint

admin = Blueprint(
    "admin",                     # name: prefix of every endpoint, no dots, not empty
    __name__,                    # import_name: locates root_path
    static_folder="static",      # relative to root_path; no blueprint static route without it
    static_url_path=None,        # URL of the static folder; defaults to the folder's last path part
    template_folder="templates", # added to the template search path
    url_prefix="/admin",         # prepended to every rule of the blueprint
    subdomain=None,              # match this subdomain by default
    url_defaults={"section": "admin"},  # default values for every view of the blueprint
    root_path=None,              # override the folder detected from import_name
    cli_group=None,              # name of the CLI group for bp.cli commands
)
assert admin.name == "admin" and admin.url_prefix == "/admin"
assert admin.template_folder == "templates" and admin.static_folder.endswith("static")

Every parameter, with the behaviour the Flask source and documentation give it:

Parameter Default Meaning

name

required

Prepended, with a dot, to each endpoint. May not be empty or contain .. When the blueprint is registered with a name= option or nested, the registered name differs from this one (see Registering the Same Blueprint Twice and Nested Blueprints).

import_name

required

The module or package the blueprint belongs to, usually __name__. Flask uses it to find root_path, the folder that static_folder and template_folder are relative to. When it points to a package, the package folder is the root; when it is a module, the folder that contains the module is.

static_folder

None

Folder with static files, absolute or relative to root_path. Blueprint static files are disabled unless this is set.

static_url_path

the last part of static_folder

URL path of the static route, relative to the blueprint’s URL prefix. See Static Files per Blueprint.

template_folder

None

Folder added to the application’s template search path, absolute or relative to root_path. Blueprint templates are disabled unless this is set. See Templates per Blueprint.

url_prefix

None

Prepended to every rule. A trailing slash is stripped when the blueprint is registered, and a slash between the prefix and a rule is merged, so "/books/" plus "/<isbn>" gives /books/<isbn>.

subdomain

None

Subdomain the routes match by default. Needs subdomain matching enabled on the application, see Subdomains.

url_defaults

None

A dict of default values passed to every view of the blueprint, as if each rule had been given defaults=.

root_path

detected

Set manually when the automatic detection from import_name fails in a particular situation.

cli_group

unset (the blueprint’s name)

Name of the Click group the blueprint’s bp.cli commands live under; None merges them into the top level. See Blueprint CLI Commands.

Since Flask 3.0 Blueprint and Flask both derive from Sans-IO base classes (flask.sansio), the version-neutral core that Quart builds on. Application code does not notice; the page mentions it because tracebacks and extension code show the sansio module paths.

register_blueprint and Its Options

app.register_blueprint(bp, **options) and parent_bp.register_blueprint(child, **options) accept options that override what the blueprint was created with:

Option Effect

url_prefix

Replaces the blueprint’s own url_prefix for this registration.

subdomain

Replaces the blueprint’s own subdomain.

url_defaults

Merged over the blueprint’s own url_defaults (the registration’s values win).

name

The registered name, which replaces the constructor name in every endpoint. Two registrations of one blueprint must use different names (2.0.1). See Registering the Same Blueprint Twice.

cli_group

Overrides the blueprint’s cli_group for this registration.

any other keyword

Passed through untouched to the BlueprintSetupState object, where record() callbacks can read it as state.options. This is how an extension blueprint receives its own settings. See What Happens When a Blueprint Is Registered.

The same catalog blueprint mounted under other names, to show each option:

from flask import Blueprint, Flask, url_for

bp = Blueprint("shelf", __name__)


@bp.get("/<name>")
def shelf(name, tone="plain"):
    return f"{name} ({tone})"


app = Flask("options-demo")
app.register_blueprint(bp, url_prefix="/shelves", url_defaults={"tone": "wood"})
app.register_blueprint(bp, url_prefix="/archive", url_defaults={"tone": "stone"}, name="archive")

client = app.test_client()
assert client.get("/shelves/fiction").text == "fiction (wood)"
assert client.get("/archive/fiction").text == "fiction (stone)"
assert sorted(app.blueprints) == ["archive", "shelf"]

with app.test_request_context():
    assert url_for("shelf.shelf", name="poetry") == "/shelves/poetry"
    assert url_for("archive.shelf", name="poetry") == "/archive/poetry"

app.blueprints is a dict of registered name to blueprint; app.iter_blueprints() iterates the blueprints in registration order. A url_prefix given at registration beats the one given to the constructor, which is what lets the factory (not the blueprint’s author) decide where a blueprint lives: app.register_blueprint(api_bp, url_prefix="/api") in the Bookshelf factory. Keeping the prefix in one place, the registration, makes URL layout a deployment decision; a prefix in the constructor is better when the blueprint only ever makes sense under that prefix (the catalog blueprint above, /books).

What Happens When a Blueprint Is Registered

Understanding the mechanism removes most of the surprises in the rest of the page. Decorating a function on a blueprint appends a closure to bp.deferred_functions (through record()); a handful of data structures such as error handlers and before_request lists are stored on the blueprint under its own name. Blueprint.register(app, options) then:

  1. Builds the registered name: the name option (or the constructor name), prefixed by the parent’s name when nested (parent.child). If that name is already in app.blueprints, it raises ValueError (2.1) and tells you to use name=.

  2. Records the blueprint in app.blueprints and marks it as registered. From here on the setup methods of the blueprint raise AssertionError (2.3; they warned in 2.2).

  3. Creates a BlueprintSetupState, which resolves the effective subdomain, URL prefix and URL defaults.

  4. Adds the blueprint’s static route if it has a static folder.

  5. Merges the blueprint’s error handlers, view functions, before_request, after_request, teardown_request, URL default, URL value preprocessor and context processor lists into the application’s, keyed by the registered name. This happens for the first registration of the blueprint and for the first registration of each new name.

  6. Calls every deferred function with the state. These are the add_url_rule calls that create the URL rules, and the record_once callbacks of the app_* decorators.

  7. Wires the blueprint’s CLI group into app.cli.

  8. Registers nested blueprints, passing along the combined prefix, subdomain and name.

Nothing here touches a request: the whole process happens at set-up time, which is why registration must be finished before the first request.

from flask import Blueprint, Flask

demo = Blueprint("demo", __name__)
seen = []


@demo.record
def on_every_registration(state):
    seen.append(("record", state.name, state.url_prefix, state.first_registration))


@demo.record_once
def on_first_registration(state):
    seen.append(("record_once", state.name))


@demo.record
def read_custom_option(state):
    # options that register_blueprint does not know are passed through
    seen.append(("option", state.options.get("audit_tag")))


app = Flask("record-demo")
app.register_blueprint(demo, url_prefix="/one", audit_tag="first")
app.register_blueprint(demo, url_prefix="/two", name="demo2", audit_tag="second")

assert ("record", "demo", "/one", True) in seen
assert ("record", "demo2", "/two", False) in seen
assert seen.count(("record_once", "demo")) == 1
assert [t for t in seen if t[0] == "option"] == [("option", "first"), ("option", "second")]
assert not any(t[0] == "record_once" and t[1] == "demo2" for t in seen)

record(func) runs func(state) at every registration; record_once(func) runs it only the first time the blueprint object is registered. BlueprintSetupState exposes app, blueprint, options, first_registration, subdomain, url_prefix, url_defaults and add_url_rule. Application code rarely needs record; extension authors use it to read registration options and to attach things to the application once.

Templates per Blueprint

template_folder adds a folder to the Jinja search path, with lower priority than the application’s own templates folder. When several blueprints provide the same relative template path, the first registered blueprint wins. These two rules have a practical consequence: a blueprint template can be overridden by the application (a feature), and two blueprints that both ship index.html collide (a bug waiting to happen). The convention that avoids both is to repeat the blueprint name inside the folder, so that admin/index.html cannot be mistaken for any other index.html:

src/bookshelf/
  admin/
    __init__.py            # bp = Blueprint("admin", __name__, template_folder="templates")
    templates/
      admin/
        index.html         # render_template("admin/index.html")
  templates/               # the application's folder: wins on a name clash
    base.html
    admin/
      index.html           # overrides the blueprint's own page

The following example builds this layout in a temporary folder, so that it is runnable, and shows both rules:

import pathlib
import tempfile

from flask import Blueprint, Flask, render_template

work = pathlib.Path(tempfile.mkdtemp(prefix="bookshelf-bp-"))


def write(relative, text):
    path = work / relative
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(text, encoding="utf-8")


write("admin/templates/admin/index.html", "admin home from the blueprint")
write("admin/templates/shared/footer.html", "footer from admin")
write("reports/templates/shared/footer.html", "footer from reports")
write("app_templates/admin/index.html", "admin home overridden by the application")

admin = Blueprint("admin", "bookshelf_demo.admin", root_path=str(work / "admin"), template_folder="templates",
                  url_prefix="/admin")
reports = Blueprint("reports", "bookshelf_demo.reports", root_path=str(work / "reports"),
                    template_folder="templates", url_prefix="/reports")


@admin.get("/")
def index():
    return render_template("admin/index.html")


@admin.get("/footer")
def admin_footer():
    return render_template("shared/footer.html")


app = Flask("templates-demo", root_path=str(work), template_folder="app_templates")
app.register_blueprint(reports)  # registered first: its shared/footer.html wins
app.register_blueprint(admin)

client = app.test_client()
# 1. the application's folder has priority over the blueprint's
assert client.get("/admin/").text == "admin home overridden by the application"
# 2. between blueprints, the first registered wins on a name clash
assert client.get("/admin/footer").text == "footer from reports"

(The root_path arguments only exist to build the example in a temporary folder; in a real package __name__ is enough.) When a template is not found or the wrong one is rendered, set EXPLAIN_TEMPLATE_LOADING to True in the configuration. Flask then logs, on every render_template call, each loader and blueprint it tried, which one found the file and which ones were skipped. Jinja caches compiled templates, so the lookup is only logged the first time a template is loaded:

import logging

from flask import render_template

app.config["EXPLAIN_TEMPLATE_LOADING"] = True
app.logger.setLevel(logging.INFO)
records = []


class Collect(logging.Handler):
    def emit(self, record):
        records.append(record.getMessage())


app.logger.addHandler(Collect())
app.jinja_env.cache.clear()  # Jinja caches compiled templates; only a cache miss consults the loaders
with app.test_request_context():
    render_template("shared/footer.html")
assert any("shared/footer.html" in line for line in records)
assert any("reports" in line for line in records)
app.config["EXPLAIN_TEMPLATE_LOADING"] = False

Context processors follow the same split as hooks: @bp.context_processor applies to templates rendered by views of the blueprint, @bp.app_context_processor to every template (see App-Wide Hooks from a Blueprint). Templates and Jinja themselves, including filters and inheritance, belong to the Jinja Templates page of this section.

Static Files per Blueprint

A blueprint serves static files when it is given static_folder. By default the rightmost part of the folder path is the URL segment, appended to the blueprint’s URL prefix; static_url_path changes it. The endpoint is <blueprint name>.static, and url_for builds URLs for it as for the application’s own static folder:

from flask import Blueprint, Flask, url_for

write("admin/static/admin.css", "body { background: #eee }")
write("assets/theme.css", "body { color: #111 }")

static_admin = Blueprint("admin", "bookshelf_demo.admin", root_path=str(work / "admin"),
                         static_folder="static", url_prefix="/admin")
static_assets = Blueprint("assets", "bookshelf_demo.assets", root_path=str(work / "assets"),
                          static_folder=str(work / "assets"), static_url_path="/theme")  # no url_prefix

app2 = Flask("static-demo")
app2.register_blueprint(static_admin)
app2.register_blueprint(static_assets)

client = app2.test_client()
assert client.get("/admin/static/admin.css").status_code == 200
# the blueprint without a url_prefix and a static_url_path of its own is fine...
assert client.get("/theme/theme.css").status_code == 200
with app2.test_request_context():
    assert url_for("admin.static", filename="admin.css") == "/admin/static/admin.css"
    assert url_for("assets.static", filename="theme.css") == "/theme/theme.css"

The trap is the default static folder name without a prefix. If the blueprint has no url_prefix and keeps the default URL, its static route would be /static/<path:filename>, which the application’s own /static route already owns. The application’s route takes precedence, and, unlike templates, a blueprint static folder is not searched when the file is missing from the application’s static folder. The page of this section on static files covers the application’s own folder, cache headers and send_from_directory.

clash = Blueprint("clash", "bookshelf_demo.admin", root_path=str(work / "admin"), static_folder="static")
app3 = Flask("clash-demo")
app3.register_blueprint(clash)
# /static/admin.css exists only in the blueprint's folder and the application's route wins: 404
assert app3.test_client().get("/static/admin.css").status_code == 404

Static folders are public. Everything in static_folder is downloadable by anyone who can reach the URL, so a blueprint folder never holds source files, templates, configuration or uploads that need access control. Flask serves through send_from_directory, which rejects paths that escape the folder.

Building URLs: Absolute and Relative Endpoints

url_for takes the full endpoint, blueprint name and dot included: url_for("admin.index"). Inside a view or a template that is being rendered for a view of the same blueprint, a relative endpoint starts with a dot and is resolved against the current blueprint: url_for(".index") means admin.index when the current request was dispatched to the admin blueprint, and the same text reaches catalog.index in the catalog. Relative names make a blueprint independent of the name it was registered under, which matters for Registering the Same Blueprint Twice and Nested Blueprints.

from flask import Blueprint, Flask, render_template_string, request, url_for

pages = Blueprint("pages", __name__, url_prefix="/pages")


@pages.get("/")
def index():
    # request.blueprint is the registered name, request.blueprints lists it with its parents
    return f"{request.blueprint}|{url_for('.about')}|{url_for('pages.about')}"


@pages.get("/about")
def about():
    return render_template_string("<a href=\"{{ url_for('.index') }}\">home</a>")


app = Flask("urls-demo")
app.register_blueprint(pages)
client = app.test_client()
assert client.get("/pages/").text == "pages|/pages/about|/pages/about"
assert client.get("/pages/about").text == '<a href="/pages/">home</a>'

with app.test_request_context("/"):  # a request that matched no blueprint
    from werkzeug.routing import BuildError

    try:
        url_for(".about")
    except BuildError as error:
        assert "about" in str(error)

Two pitfalls. A relative endpoint outside a blueprint request (a background job, a CLI command, a test request that matched nothing) has no blueprint to resolve against and raises BuildError; use the absolute endpoint there. And request.blueprint is the registered name, which may differ from the name passed to the constructor when the blueprint is nested or registered with name=. request.blueprints lists the registered names from the matched blueprint up through its parents (request.blueprints was added in 2.0.1).

URL Prefixes with Parameters

A URL prefix can contain converters, and the values become view arguments of every view in the blueprint. Combined with the blueprint’s own url_defaults and url_value_preprocessor functions, this removes repeated plumbing such as a language code in every URL:

from flask import Blueprint, Flask, g, url_for

frontend = Blueprint("frontend", __name__, url_prefix="/<lang_code>")


@frontend.url_defaults
def add_language_code(endpoint, values):
    values.setdefault("lang_code", g.lang_code)


@frontend.url_value_preprocessor
def pull_language_code(endpoint, values):
    g.lang_code = values.pop("lang_code")


@frontend.get("/")
def index():
    return f"home in {g.lang_code}"


@frontend.get("/about")
def about():
    return f"about in {g.lang_code}: {url_for('.index')}"


app = Flask("lang-demo")
app.register_blueprint(frontend)
client = app.test_client()
assert client.get("/es/").text == "home in es"
assert client.get("/en/about").text == "about in en: /en/"

@bp.url_value_preprocessor runs after the URL was matched, only for requests of that blueprint, and @bp.url_defaults runs whenever url_for builds a URL for one of its endpoints. The app_url_value_preprocessor and app_url_defaults variants apply to every endpoint (see App-Wide Hooks from a Blueprint). The same idea, with a language-aware Flask-Babel setup, is the language-prefix recipe of the internationalisation page of this section.

Note that url_defaults also exists as a Blueprint constructor argument and as a register_blueprint option, with a different meaning: those are plain default values injected as rule defaults, while the decorator is a function that can compute values per URL building call.

Subdomains

subdomain on the constructor or on register_blueprint makes the blueprint’s routes match a subdomain. Flask ignores the subdomain unless it is told to consider it, with Flask(__name__, subdomain_matching=True) and a SERVER_NAME configured; host_matching=True is the alternative for entirely different hosts. Without that, rules built for a subdomain never match and the request is a 404:

from flask import Blueprint, Flask, url_for

api = Blueprint("api", __name__, subdomain="api")


@api.get("/ping")
def ping():
    return "pong"


matching = Flask("sub-demo", subdomain_matching=True)
matching.config["SERVER_NAME"] = "bookshelf.test"
matching.register_blueprint(api)
c = matching.test_client()
assert c.get("/ping", base_url="http://api.bookshelf.test").text == "pong"
assert c.get("/ping", base_url="http://bookshelf.test").status_code == 404
with matching.test_request_context(base_url="http://bookshelf.test"):
    assert url_for("api.ping", _external=True) == "http://api.bookshelf.test/ping"

plain = Flask("sub-demo-off")  # subdomain_matching defaults to False
plain.config["SERVER_NAME"] = "bookshelf.test"
plain.register_blueprint(api)
assert plain.test_client().get("/ping", base_url="http://api.bookshelf.test").status_code == 404

Subdomain routing depends on the Host header, which the client controls. Behind a proxy, configure the forwarded host correctly and, since 3.1, set TRUSTED_HOSTS so a request with an unexpected host is rejected. Do not use a subdomain or a URL prefix as the only thing that separates privileged from unprivileged code; attach access checks to the blueprint (see Security Notes). SESSION_COOKIE_DOMAIN does not fall back to SERVER_NAME since 2.3, so a session shared across subdomains has to set the cookie domain explicitly. The flask routes command shows each rule’s subdomain or host when domain matching is in use.

Nested Blueprints

A blueprint can be registered on another blueprint with parent.register_blueprint(child) (2.0). The child’s name gets the parent’s name as a prefix, its URLs get the parent’s URL prefix, its subdomain is combined with the parent’s (subdomain applied correctly since 2.3), and the parent’s hooks run for the child’s requests:

from flask import Blueprint, Flask, abort, jsonify, request, url_for

api = Blueprint("api", __name__, url_prefix="/api")
v1 = Blueprint("v1", __name__, url_prefix="/v1")
trace = []


@api.before_request
def api_hook():
    trace.append(f"api before ({request.blueprint})")


@v1.before_request
def v1_hook():
    trace.append("v1 before")


@api.errorhandler(404)
def api_not_found(error):
    return jsonify(error="not found", handler="api"), 404


@v1.get("/books")
def list_books():
    return jsonify(books=[], blueprints=request.blueprints)


@v1.get("/missing")
def missing():
    abort(404)  # raised inside the nested blueprint: no handler there, the parent's runs


api.register_blueprint(v1)
app = Flask("nested-demo")
app.register_blueprint(api)

client = app.test_client()
assert client.get("/api/v1/books").get_json()["blueprints"] == ["api.v1", "api"]
assert trace == ["api before (api.v1)", "v1 before"]  # parent first, then the child
assert client.get("/api/v1/missing").get_json()["handler"] == "api"
assert sorted(app.blueprints) == ["api", "api.v1"]
with app.test_request_context():
    assert url_for("api.v1.list_books") == "/api/v1/books"

The rules in detail:

  • Endpoints are dotted: api.v1.list_books. The registered name in app.blueprints is api.v1. Since 2.0.1 two different blueprints with the same constructor name can be nested at different places without a clash.

  • URL prefixes combine: /api plus /v1 plus the rule. A url_prefix option given when registering the child combines with the parent’s prefix the same way.

  • Subdomains combine in the other direction: a child with subdomain="child" inside a parent with subdomain="parent" answers on child.parent.<server name>.

  • Hooks run from the application down to the nested blueprint (2.0.2): application before_request, the parent’s, then the child’s. After-request hooks run in the reverse order.

  • Error handlers: if the child has no handler that can handle the exception, the parent’s is tried, and then the application’s (see Blueprint Error Handlers and Their Limits for the order within one level).

  • Setup after registration is an error in the child too: a nested blueprint is registered when its parent is.

Because a nested blueprint is registered when its parent is, the order of the two register_blueprint calls above matters: api.register_blueprint(v1) must run before app.register_blueprint(api), because register_blueprint is a setup method and raises the setup-after-registration error once api is registered (see Setup After Registration Is an Error). The factory is the natural place to assemble the whole tree.

Registering the Same Blueprint Twice

Mounting one blueprint at several places is what name= is for. Each registration needs a unique name, because the name is how Flask tells the endpoints apart and url_for picks them. Without name=, the second registration raises ValueError (since 2.1 this is an error; it was a deprecation before). The Bookshelf pages blueprint serves the same static pages in two languages:

from flask import Blueprint, Flask, url_for

pages = Blueprint("pages", __name__)


@pages.get("/")
def home(lang="en"):
    return f"home ({lang}) at {url_for('.home')}"


app = Flask("twice-demo")
app.register_blueprint(pages, url_prefix="/en", name="pages_en", url_defaults={"lang": "en"})
app.register_blueprint(pages, url_prefix="/es", name="pages_es", url_defaults={"lang": "es"})

client = app.test_client()
assert client.get("/en/").text == "home (en) at /en/"
assert client.get("/es/").text == "home (es) at /es/"
assert {"pages_en", "pages_es"} <= set(app.blueprints)

duplicate = Flask("twice-demo-duplicate")
duplicate.register_blueprint(pages, url_prefix="/de")  # registered as "pages"
try:
    duplicate.register_blueprint(pages, url_prefix="/fr")  # "pages" again: error
except ValueError as error:
    assert "name" in str(error)
else:
    raise AssertionError("expected ValueError")

The same example shows why relative endpoints matter: url_for(".home") returns the URL of whichever registration handled the request, /en/ or /es/, while url_for("pages.home") would always mean the registration called pages. A blueprint written to be mounted more than once should use relative endpoints and avoid assumptions such as unique endpoint names, and the documentation says plainly that not every blueprint can be mounted twice because it depends on how it is implemented. record_once callbacks and the app_* decorators run only for the first registration of the blueprint object, so there is one global hook, not two.

App-Wide Hooks from a Blueprint

A decorator such as bp.before_request applies only to requests whose endpoint belongs to the blueprint (and its nested children). The app_ variants register on the application and apply to every request, whichever blueprint or plain route handles it. That lets a blueprint carry the cross-cutting code it needs, such as an audit log, a shared error format or template helpers, without the factory knowing about each item.

Blueprint-scoped App-wide counterpart What it does

before_request

before_app_request

Runs before the view; returning a response skips the view.

after_request

after_app_request

Receives and returns the response.

teardown_request

teardown_app_request

Runs at the end of the request, with the exception if one occurred.

errorhandler

app_errorhandler

Handles an exception class or HTTP status code.

context_processor

app_context_processor

Adds variables to the template context.

url_defaults

app_url_defaults

Injects values into url_for.

url_value_preprocessor

app_url_value_preprocessor

Processes URL values after matching.

not available

app_template_filter, app_template_test, app_template_global

Registers a Jinja filter, test or global for the whole application (there is no scoped version; templates are shared).

The Bookshelf core blueprint contributes request timing, a JSON-aware error handler and a context processor to the whole application:

import time

from flask import Blueprint, Flask, g, jsonify, render_template_string, request

core = Blueprint("core", __name__)


@core.before_app_request
def start_timer():
    g.started = time.perf_counter()


@core.after_app_request
def add_timing(response):
    response.headers["X-Elapsed-Ms"] = f"{(time.perf_counter() - g.started) * 1000:.2f}"
    return response


@core.app_errorhandler(404)
def not_found(error):
    if request.path.startswith("/api/"):
        return jsonify(error="not found"), 404
    return "no such page", 404


@core.app_context_processor
def inject_site():
    return {"site_name": "Bookshelf"}


@core.app_template_filter("isbn")
def format_isbn(value):
    return f"{value[:3]}-{value[3:]}"


@core.get("/")
def home():
    return render_template_string("{{ site_name }}: {{ '9780132350884'|isbn }}")


other = Blueprint("other", __name__, url_prefix="/other")


@other.get("/")
def other_index():
    return render_template_string("{{ site_name }}")  # the context processor of core reaches every template


app = Flask("hooks-demo")
app.register_blueprint(core)
app.register_blueprint(other)
client = app.test_client()
home_response = client.get("/")
assert home_response.text == "Bookshelf: 978-0132350884"
assert "X-Elapsed-Ms" in home_response.headers
assert "X-Elapsed-Ms" in client.get("/other/").headers  # hooks apply to other blueprints' requests
assert client.get("/other/").text == "Bookshelf"
assert client.get("/api/nothing").get_json() == {"error": "not found"}  # app_errorhandler catches routing 404s
assert client.get("/nothing").text == "no such page"

Points to remember:

  • The app_ hooks fire for every request, including those that matched no blueprint at all and including requests to the static route. Keep them cheap and defensive (note g.started above exists because before_app_request always ran first).

  • Registering an app_ hook in a blueprint that is mounted twice installs it once, because it uses record_once.

  • The old before_app_first_request blueprint decorator was removed in 2.3 together with before_first_request. Do one-time work in the factory.

  • Order is registration order of the blueprints, then definition order inside each blueprint, for before_request and context_processor functions; after_request and teardown functions run in the reverse order.

Blueprint Error Handlers and Their Limits

@bp.errorhandler(code_or_exception) and bp.register_error_handler(...) work as on the application, with a lookup order since 1.0 of: blueprint handler for the status code, application handler for the code, blueprint handler for the exception class, application handler for the exception class. A blueprint handler is considered only when the exception was raised while handling a request that belongs to the blueprint.

Handlers for 404 and 405 are the exception. A routing failure happens before any blueprint has been chosen, because a blueprint does not own a URL space; Flask cannot tell which blueprint’s handler to use for an unknown URL. A blueprint-level 404 or 405 handler therefore only runs when a view of that blueprint raises it, with abort(404) or a NotFound exception, not for an invalid URL below the blueprint’s prefix. To give the JSON API its own error body, register the handler on the application and branch on the path, as the documentation does, or use app_errorhandler from the blueprint that owns the API format:

from flask import Blueprint, Flask, abort, jsonify, request

api = Blueprint("api", __name__, url_prefix="/api")


@api.errorhandler(404)
def api_404(error):
    return jsonify(error="api handler"), 404


@api.get("/books/<int:book_id>")
def get_book(book_id):
    if book_id != 1:
        abort(404)
    return jsonify(id=1)


app = Flask("limits-demo")
app.register_blueprint(api)
client = app.test_client()
assert client.get("/api/books/2").get_json() == {"error": "api handler"}  # raised by a view of the blueprint
assert client.get("/api/nope").get_json() is None  # a routing 404: the blueprint handler is NOT used
assert client.get("/api/nope").status_code == 404

# the fix: one application-level handler that branches on the path
fixed = Flask("limits-demo-fixed")
fixed.register_blueprint(api)


@fixed.errorhandler(404)
@fixed.errorhandler(405)
def handle_api_error(error):
    if request.path.startswith("/api/"):
        return jsonify(error=error.name, code=error.code), error.code
    return error


fixed_client = fixed.test_client()
assert fixed_client.get("/api/nope").get_json() == {"error": "Not Found", "code": 404}
assert fixed_client.post("/api/books/1").get_json() == {"error": "Method Not Allowed", "code": 405}

Two details of that example. The handler returns error itself for non-API paths, which hands the HTTPException back to Flask so the default page is produced. And the application-level handler is added before the first request: adding one afterwards raises the setup error of Setup After Registration Is an Error, which is why the example builds a second application instead of patching the first. The error handling page of this section covers handler lookup, abort, JSON errors and HTTPException in full.

Blueprint CLI Commands

bp.cli is a Click group (AppGroup) attached to the blueprint. Commands registered on it become available under the flask command once the blueprint is registered, nested in a group named after the blueprint’s registered name. An application context is active inside the commands, and @with_appcontext is not needed (since 2.2):

import click
from flask import Blueprint, Flask, current_app

students = Blueprint("students", __name__)
catalog_cli = Blueprint("catalog", __name__, cli_group=None)  # merge into the top level
renamed = Blueprint("shelves", __name__, cli_group="stock")


@students.cli.command("create")
@click.argument("name")
def create_student(name):
    click.echo(f"created {name} in {current_app.name}")


@catalog_cli.cli.command("reindex")
def reindex():
    click.echo("reindexed")


@renamed.cli.command("count")
def count():
    click.echo("42 books")


app = Flask("cli-demo")
app.register_blueprint(students)
app.register_blueprint(catalog_cli)
app.register_blueprint(renamed)
runner = app.test_cli_runner()

assert runner.invoke(args=["students", "create", "alice"]).output == "created alice in cli-demo\n"
assert runner.invoke(args=["reindex"]).output == "reindexed\n"
assert runner.invoke(args=["stock", "count"]).output == "42 books\n"

In a terminal these are flask students create alice, flask reindex and flask stock count. The group name can be changed in the Blueprint constructor or per registration, app.register_blueprint(bp, cli_group="other"), and cli_group=None removes the nesting so that the commands join the application level, at the risk of colliding with a built-in or another blueprint’s command. Commands that do not depend on the application belong in a plain Click group registered with the factory or in a console script; the CLI page of this section covers AppGroup, FlaskGroup, and app.test_cli_runner() in depth.

Blueprint Resources: root_path and open_resource

A blueprint is "contained in a folder", the one named by import_name: the package folder if import_name is a package, the folder holding the module otherwise. bp.root_path shows it, and bp.open_resource(path) opens a file relative to it (with an encoding parameter in text mode since 3.1). It is the right way to ship data files, SQL scripts or e-mail text with a blueprint, because it keeps working when the project is installed in site-packages.

from flask import Blueprint

res = Blueprint("res", "bookshelf_demo.res", root_path=str(work / "admin"))
assert res.root_path == str(work / "admin")
with res.open_resource("static/admin.css", mode="r", encoding="utf-8") as handle:
    assert handle.read().startswith("body")

While several blueprints can originate from the same folder, the documentation notes it is usually not recommended: static_folder and template_folder are relative to root_path, so two blueprints sharing a folder also share the default names and end up fighting over them.

A Blueprint as a Package: Views, Models and Forms Together

The feature-per-package layout puts the blueprint object, its views, forms, services, templates and (if it owns them) models in one directory, and exposes just bp to the factory:

src/bookshelf/catalog/
  __init__.py       # from .routes import bp
  routes.py         # bp = Blueprint("catalog", __name__, ...) and the views
  forms.py          # BookForm (Flask-WTF)
  services.py       # CatalogService
  templates/catalog/
    list.html
    detail.html

The failure to avoid is a circular import. routes.py needs bp, __init__.py needs the views to be registered, and the views need the extensions and models. Create the blueprint object before anything that decorates it is imported, keep extension objects in an extensions.py that imports nothing from the blueprints, and let the factory import the package inside create_app (the shape of the Bookshelf factory). The example builds a package with that shape and registers it:

import importlib
import sys

write("bookshelf_demo/__init__.py", "")
write("bookshelf_demo/catalog/__init__.py", "from .routes import bp\n\n__all__ = ['bp']\n")
write("bookshelf_demo/catalog/forms.py", "REQUIRED = ('title', 'author')\n")
write(
    "bookshelf_demo/catalog/routes.py",
    "from flask import Blueprint, jsonify, request\n"
    "from .forms import REQUIRED\n\n"
    "bp = Blueprint('catalog', __name__, url_prefix='/books')\n\n\n"
    "@bp.post('/')\n"
    "def create():\n"
    "    data = request.get_json()\n"
    "    missing = [f for f in REQUIRED if f not in data]\n"
    "    if missing:\n"
    "        return jsonify(missing=missing), 422\n"
    "    return jsonify(data), 201\n",
)
sys.path.insert(0, str(work))
try:
    package = importlib.import_module("bookshelf_demo.catalog")
    assert package.bp.root_path.endswith("catalog")  # the package folder is the resource folder
    factory_app = Flask("package-demo")
    factory_app.register_blueprint(package.bp)
    client = factory_app.test_client()
    assert client.post("/books/", json={"title": "Dune"}).get_json() == {"missing": ["author"]}
    assert client.post("/books/", json={"title": "Dune", "author": "Herbert"}).status_code == 201
finally:
    sys.path.remove(str(work))
    for name in [m for m in sys.modules if m.startswith("bookshelf_demo")]:
        del sys.modules[name]

The same rule applies to the JSON API blueprint that serves a single-page application: it is a normal blueprint (api, mounted at /api, with its own error handlers returning JSON) and, nested or not, it is where the front end’s cross-origin and CSRF decisions meet the back end. A blueprint can be exempted wholesale from Flask-WTF’s CSRFProtect with csrf.exempt(api_bp), which is the right tool for a token-authenticated API and the wrong one for cookie-authenticated pages. The single-page-applications page of this section covers CORS and cookies, and the Flask-WTF page covers CSRF tokens.

A JSON API Blueprint for a Single-Page App

The back end of a single-page application is usually one more blueprint, mounted under /api, whose error handlers speak JSON instead of HTML. The extensions it uses (db, a serializer) were created unbound in extensions.py and bound with init_app in the factory before register_blueprint, so that the blueprint module can import them without a cycle:

from flask import Blueprint, Flask, abort, jsonify, request, url_for
from werkzeug.exceptions import HTTPException

api_bp = Blueprint("api", __name__, url_prefix="/api")
SHELF = {}


@api_bp.errorhandler(HTTPException)
def json_http_error(error):
    return jsonify(error=error.name, status=error.code), error.code


@api_bp.get("/books/<int:book_id>")
def read_book(book_id):
    if book_id not in SHELF:
        abort(404)
    return jsonify(SHELF[book_id])


@api_bp.post("/books")
def create_book():
    payload = request.get_json()  # 415 if the content type is not JSON, 400 if the body is invalid
    book_id = len(SHELF) + 1
    SHELF[book_id] = {"id": book_id, "title": payload["title"]}
    return jsonify(SHELF[book_id]), 201, {"Location": url_for(".read_book", book_id=book_id)}


def create_app():
    app = Flask("spa-demo")
    # extensions: db.init_app(app) and friends would go here, before the blueprints
    app.register_blueprint(api_bp)
    return app


client = create_app().test_client()
created = client.post("/api/books", json={"title": "Dune"})
assert created.status_code == 201 and created.headers["Location"] == "/api/books/1"
assert client.get("/api/books/1").get_json() == {"id": 1, "title": "Dune"}
assert client.get("/api/books/9").get_json() == {"error": "Not Found", "status": 404}
assert client.post("/api/books", data="x", content_type="text/plain").get_json()["status"] == 415

The handler is registered for the base class HTTPException, so every HTTP error that a view of this blueprint raises (abort, a bad JSON body, a failed get_json()) is rendered as JSON. Routing errors for unknown URLs are still answered by the application, for the reason explained in Blueprint Error Handlers and Their Limits. The dev-server proxy, CORS and cookie authentication choices that belong with this blueprint are covered by the single-page-applications page of this section.

Blueprints Versus Separate Applications

Both blueprints and several application objects can split a large system. They differ in what is shared:

Blueprints Separate applications (dispatching)

Configuration

One app.config shared by all.

One configuration per application.

Where the separation happens

In Flask: one URL map, endpoint prefixes, shared extensions.

In the WSGI layer: a dispatcher such as Werkzeug’s DispatcherMiddleware or a reverse proxy picks the application.

Cost of a request

One application, one set of contexts.

One application per mount; a request enters exactly one.

Failure isolation

None: an exception in a blueprint is an exception in the application.

Independent applications can be deployed, scaled and restarted separately.

Removing a part

Not possible once registered; build the application without it.

Drop the mount.

Typical use

Organising one product: catalog, auth, admin, API.

Combining products, or isolating security domains, each with its own secret key.

Start with blueprints. Move to separate applications when the parts need different configuration, different dependencies or independent deployment, at which point they usually become separate services; the factory page of this section shows DispatcherMiddleware and the Microservice boundaries discussion, and Architectural Patterns covers how to decide.

Setup After Registration Is an Error

Everything a blueprint records must be recorded before it is registered, and the application’s setup methods must run before it handles its first request. Since Flask 2.3 breaking either rule raises AssertionError (a warning in 2.2):

from flask import Blueprint, Flask

late = Blueprint("late", __name__)
app = Flask("late-demo")
app.register_blueprint(late)

try:
    late.get("/too-late")(lambda: "x")  # a setup method after registration
except AssertionError as error:
    assert "can no longer be called on the blueprint" in str(error)
else:
    raise AssertionError("expected AssertionError")

app.test_client().get("/")  # the first request
try:
    app.register_blueprint(Blueprint("later", __name__))
except AssertionError as error:
    assert "can no longer be called on the application" in str(error)
else:
    raise AssertionError("expected AssertionError")

This is why decorators on a blueprint live at module import time (or at least before registration), why a nested blueprint must be attached to its parent before the parent is registered, and why registration belongs in the factory, before the application receives traffic. The old workarounds, before_first_request and before_app_first_request, were removed in 2.3.

Testing Blueprints

A blueprint is tested through the application it is registered on. Two techniques cover most needs: exercise the views with the test client, and assert the structure with app.blueprints, app.url_map and app.view_functions, which catches an accidentally dropped registration or prefix:

def build_app():
    app = Flask("structure-demo")
    app.config.update(TESTING=True)
    app.register_blueprint(catalog, url_prefix="/books")
    return app


def test_catalog_is_registered_under_books():
    app = build_app()
    assert "catalog" in app.blueprints
    rules = {rule.endpoint: rule.rule for rule in app.url_map.iter_rules()}
    assert rules["catalog.list_books"] == "/books/"
    assert "catalog.show_book" in app.view_functions
    assert app.test_client().get("/books/").status_code == 200


test_catalog_is_registered_under_books()

Because a blueprint cannot be unregistered, each test builds a new application (a pytest fixture around the factory, as on the factory page). A blueprint that reads the database or an extension is tested against the factory’s TESTING configuration, not against a mock of the application.

Security Notes

  • A URL prefix is not access control. url_prefix="/admin" only changes URLs. Protect a privileged blueprint with a @bp.before_request function that checks authentication and authorisation, and returns or aborts with 401 or 403 (see the authentication and authorisation pages of this section). Nested blueprints inherit the parent’s hook, so putting the check on the top-level admin blueprint covers every child.

  • before_request runs only for matched endpoints. An unknown URL under /admin/ is answered by the routing 404, not by the blueprint’s hook; that is harmless for authorisation but means a 404 is not proof a path is unprotected. Use before_app_request when something must run for every request.

  • Audit third-party blueprints. A blueprint can install app_ hooks, error handlers and context processors that affect the whole application. Read what an extension registers before registering it.

  • Static folders are public and must hold nothing that needs access control (see Static Files per Blueprint).

  • Template shadowing is a trust boundary. Because application templates override blueprint templates, a template path that is built from user input can pick a different blueprint’s template than expected. Never pass user input to render_template as a template name.

  • Do not leak internals in error handlers. Return error.name and error.code for HTTP errors (as above) and log details server-side; never put exception text from non-HTTP exceptions into a response in production.

  • Host-dependent routing (subdomains, host_matching) trusts the Host header. Configure proxy headers and TRUSTED_HOSTS (3.1) deliberately.

  • CSRF exemptions are per blueprint with Flask-WTF. Exempting a blueprint removes protection from every form in it; keep exempt blueprints to token-authenticated JSON endpoints.

The Same Job in Other Frameworks

  • FastAPI: an APIRouter plays the role of a blueprint, with prefix, tags and dependencies set when it is included, and nesting through include_router. There is no template or static folder per router. See FastAPI: Introduction & Architecture.

  • Django: an app is a Python package with models, migrations, templates and URL configuration, and include() mounts its URLs under a prefix. A Flask blueprint is closer to a Django app’s urls.py and views than to the whole app. See Django: Introduction & Architecture.

Recent and Upcoming Changes

Version Change that touches blueprints

0.7

Blueprints introduced.

1.1

Blueprints get a cli Click group and a cli_group parameter.

2.0

Blueprints can be nested (register_blueprint on a blueprint).

2.0.1

The name option on register_blueprint; nested blueprints are registered with dotted names; URL prefixes of nested blueprints combine; a blueprint name containing a dot raises an error; request.blueprints added.

2.0.2

Fixes for registering one blueprint twice with different names; before_request and similar hooks run from the application down to the closest nested blueprint.

2.1

Registering the same blueprint with the same name more than once raises ValueError (it was deprecated).

2.2

Setup methods called on a blueprint after registration warn. @with_appcontext is no longer needed on bp.cli and app.cli commands.

2.3

Setup methods after registration raise AssertionError. before_app_first_request and before_first_request, and the json_encoder and json_decoder attributes of app and blueprint, are removed. Subdomains apply correctly to nested blueprints. An empty blueprint name raises ValueError. SESSION_COOKIE_DOMAIN no longer falls back to SERVER_NAME.

3.0

Flask and Blueprint get Sans-IO base classes.

3.1

Blueprint.open_resource takes an encoding parameter. TRUSTED_HOSTS validates the host of a request.

3.2 (not released)

The unreleased changelog describes the application and request contexts being merged (RequestContext becoming a deprecated alias of AppContext) and dispatch methods receiving the current application context. No change to the Blueprint API is listed; a blueprint that only uses the decorators of this page is unaffected. Verify against the 3.2 release notes when it ships.

Source: Flask: Changes

Where the Books Are Outdated

The books in the bibliography are used for the concept narrative only; nothing here is copied from them. Where their blueprint recipes touch this page, the current approach is:

In the books Today

Blueprints shown with Blueprint(name, __name__, template_folder, static_folder, url_prefix) and registered once in app.py

Still the right constructor, now with subdomain, url_defaults, root_path and cli_group, and registered inside an application factory with register_blueprint(bp, url_prefix=..., name=...).

Registering blueprints at module level on a global app, then importing views at the bottom of app.py

Register inside create_app(); import the blueprint package inside the factory to avoid cycles.

before_first_request or before_app_first_request for one-time set-up in a blueprint

Removed in 2.3. Do one-time work in the factory or in a record_once callback.

Adding routes or hooks to a blueprint after it was registered, or registering blueprints after the app served a request

An AssertionError since 2.3 (a warning in 2.2).

One blueprint mounted twice without name=

ValueError since 2.1. Give each registration a unique name=.

Blueprint-level 404 handlers expected to catch every unknown URL under the prefix

Never worked for routing errors; handle 404 and 405 at the application level, branching on the path.

Blueprint.json_encoder or json_decoder for custom JSON

Removed in 2.3. Use the application’s JSON provider (app.json).

@with_appcontext on every blueprint command

Not needed for bp.cli and app.cli commands since 2.2.

A setup.py-based "installable app" containing the blueprint packages

pyproject.toml and pip install -e . (see the factory page). Check that the build backend includes the templates and static folders of every blueprint package as package data, or an installed wheel will render without them.

API blueprints returning json.dumps(...) strings from Flask-RESTful resources

Return dicts or use jsonify. For a documented API, flask-smorest ships a Blueprint that subclasses Flask’s, and the REST and OpenAPI pages of this section build on it.

A hard-coded placeholder string as the secret_key in the blueprint chapter examples

Never commit keys; the factory loads SECRET_KEY from the instance folder or the environment.