Blueprints
Overview#
Blueprints are objects that can be used for sub-routing within an application. Instead of adding routes to the application instance, blueprints define similar methods for adding routes, which are then registered with the application in a flexible and pluggable manner.
Blueprints are especially useful for larger applications, where your application logic can be broken down into several groups or areas of responsibility.
Creating and registering#
First, you must create a blueprint. It has a very similar API as the Sanic()
app instance with many of the same decorators.
# ./my_blueprint.py
from sanic.response import json
from sanic import Blueprint
bp = Blueprint("my_blueprint")
@bp.route("/")
async def bp_root(request):
return json({"my": "blueprint"})
Next, you register it with the app instance.
from sanic import Sanic
from my_blueprint import bp
app = Sanic(__name__)
app.blueprint(bp)
Blueprints also have the same websocket()
decorator and add_websocket_route
method for implementing websockets.
Beginning in v21.12, a Blueprint may be registered before or after adding objects to it. Previously, only objects attached to the Blueprint at the time of registration would be loaded into application instance.
app.blueprint(bp)
@bp.route("/")
async def bp_root(request):
...
Copying#
Blueprints along with everything that is attached to them can be copied to new instances using the copy()
method. The only required argument is to pass it a new name
. However, you could also use this to override any of the values from the old blueprint.
v1 = Blueprint("Version1", version=1)
@v1.route("/something")
def something(request):
pass
v2 = v1.copy("Version2", version=2)
app.blueprint(v1)
app.blueprint(v2)
Available routes:
/v1/something
/v2/something
Added in v21.9
Blueprint groups#
Blueprints may also be registered as part of a list or tuple, where the registrar will recursively cycle through any sub-sequences of blueprints and register them accordingly. The Blueprint.group method is provided to simplify this process, allowing a ‘mock’ backend directory structure mimicking what’s seen from the front end. Consider this (quite contrived) example:
api/
├──content/
│ ├──authors.py
│ ├──static.py
│ └──__init__.py
├──info.py
└──__init__.py
app.py
First blueprint#
# api/content/authors.py
from sanic import Blueprint
authors = Blueprint("content_authors", url_prefix="/authors")
Second blueprint#
# api/content/static.py
from sanic import Blueprint
static = Blueprint("content_static", url_prefix="/static")
Blueprint group#
# api/content/__init__.py
from sanic import Blueprint
from .static import static
from .authors import authors
content = Blueprint.group(static, authors, url_prefix="/content")
Third blueprint#
# api/info.py
from sanic import Blueprint
info = Blueprint("info", url_prefix="/info")
Another blueprint group#
# api/__init__.py
from sanic import Blueprint
from .content import content
from .info import info
api = Blueprint.group(content, info, url_prefix="/api")
Main server#
All blueprints are now registered
# app.py
from sanic import Sanic
from .api import api
app = Sanic(__name__)
app.blueprint(api)
Blueprint group prefixes and composability#
As shown in the code above, when you create a group of blueprints you can extend the URL prefix of all the blueprints in the group by passing the url_prefix
argument to the Blueprint.group
method. This is useful for creating a mock directory structure for your API.
In addition, there is a name_prefix
argument that can be used to make blueprints reusable and composable. The is specifically necessary when applying a single blueprint to multiple groups. By doing this, the blueprint will be registered with a unique name for each group, which allows the blueprint to be registered multiple times and have its routes each properly named with a unique identifier.
Consider this example. The routes built will be named as follows:
TestApp.group-a_bp1.route1
TestApp.group-a_bp2.route2
TestApp.group-b_bp1.route1
TestApp.group-b_bp2.route2
bp1 = Blueprint("bp1", url_prefix="/bp1")
bp2 = Blueprint("bp2", url_prefix="/bp2")
bp1.add_route(lambda _: ..., "/", name="route1")
bp2.add_route(lambda _: ..., "/", name="route2")
group_a = Blueprint.group(
bp1, bp2, url_prefix="/group-a", name_prefix="group-a"
)
group_b = Blueprint.group(
bp1, bp2, url_prefix="/group-b", name_prefix="group-b"
)
app = Sanic("TestApp")
app.blueprint(group_a)
app.blueprint(group_b)
Name prefixing added in v23.6
Middleware#
Blueprints can also have middleware that is specifically registered for its endpoints only.
@bp.middleware
async def print_on_request(request):
print("I am a spy")
@bp.middleware("request")
async def halt_request(request):
return text("I halted the request")
@bp.middleware("response")
async def halt_response(request, response):
return text("I halted the response")
Similarly, using blueprint groups, it is possible to apply middleware to an entire group of nested blueprints.
bp1 = Blueprint("bp1", url_prefix="/bp1")
bp2 = Blueprint("bp2", url_prefix="/bp2")
@bp1.middleware("request")
async def bp1_only_middleware(request):
print("applied on Blueprint : bp1 Only")
@bp1.route("/")
async def bp1_route(request):
return text("bp1")
@bp2.route("/<param>")
async def bp2_route(request, param):
return text(param)
group = Blueprint.group(bp1, bp2)
@group.middleware("request")
async def group_middleware(request):
print("common middleware applied for both bp1 and bp2")
# Register Blueprint group under the app
app.blueprint(group)
Exceptions#
Just like other exception handling, you can define blueprint specific handlers.
@bp.exception(NotFound)
def ignore_404s(request, exception):
return text("Yep, I totally found the page: {}".format(request.url))
Static files#
Blueprints can also have their own static handlers
bp = Blueprint("bp", url_prefix="/bp")
bp.static("/web/path", "/folder/to/serve")
bp.static("/web/path", "/folder/to/server", name="uploads")
Which can then be retrieved using url_for()
. See routing for more information.
>>> print(app.url_for("static", name="bp.uploads", filename="file.txt"))
'/bp/web/path/file.txt'
Listeners#
Blueprints can also implement listeners.
@bp.listener("before_server_start")
async def before_server_start(app, loop):
...
@bp.listener("after_server_stop")
async def after_server_stop(app, loop):
...
Versioning#
As discussed in the versioning section, blueprints can be used to implement different versions of a web API.
The version
will be prepended to the routes as /v1
or /v2
, etc.
auth1 = Blueprint("auth", url_prefix="/auth", version=1)
auth2 = Blueprint("auth", url_prefix="/auth", version=2)
When we register our blueprints on the app, the routes /v1/auth
and /v2/auth
will now point to the individual blueprints, which allows the creation of sub-sites for each API version.
from auth_blueprints import auth1, auth2
app = Sanic(__name__)
app.blueprint(auth1)
app.blueprint(auth2)
It is also possible to group the blueprints under a BlueprintGroup
entity and version multiple of them together at the
same time.
auth = Blueprint("auth", url_prefix="/auth")
metrics = Blueprint("metrics", url_prefix="/metrics")
group = Blueprint.group(auth, metrics, version="v1")
# This will provide APIs prefixed with the following URL path
# /v1/auth/ and /v1/metrics
Composable#
A Blueprint
may be registered to multiple groups, and each of BlueprintGroup
itself could be registered and nested further. This creates a limitless possibility Blueprint
composition.
Added in v21.6
Take a look at this example and see how the two handlers are actually mounted as five (5) distinct routes.
app = Sanic(__name__)
blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2")
group = Blueprint.group(
blueprint_1,
blueprint_2,
version=1,
version_prefix="/api/v",
url_prefix="/grouped",
strict_slashes=True,
)
primary = Blueprint.group(group, url_prefix="/primary")
@blueprint_1.route("/")
def blueprint_1_default_route(request):
return text("BP1_OK")
@blueprint_2.route("/")
def blueprint_2_default_route(request):
return text("BP2_OK")
app.blueprint(group)
app.blueprint(primary)
app.blueprint(blueprint_1)
# The mounted paths:
# /api/v1/grouped/bp1/
# /api/v1/grouped/bp2/
# /api/v1/primary/grouped/bp1
# /api/v1/primary/grouped/bp2
# /bp1
Generating a URL#
When generating a url with url_for()
, the endpoint name will be in the form:
{blueprint_name}.{handler_name}