Dynamically registering your flask blueprint endpoints
Dynamically loading your Flask's blueprint is a great way to structure your endpoints and avoid duplicating code.

Photo by Sigmund on Unsplash
Flask’s blueprint is a great way to structure your endpoints.
In the past, to avoid duplicating code, I found myself having to reuse certain blueprints several times. What changed between uses was the url prefix and certain behaviors such as filters to be applied to the data.
What I did in the past
For this purpose, I used to have “register_blueprint” functions called with different parameters depending on usage. But this wasn’t great for cyclomatic complexity as endpoint definitions were nested inside a function.
def register_blueprint(app, url_prefix, version):
blueprint = Blueprint(f"api_{version.replace('.', '_')}", __name__)
@blueprint.route("/status")
def status():
return jsonify({"status": "OK", "version": version)
if version > 2:
@blueprint.route("/something")
def something():
...
....
app.register_blueprint(blueprint, url_prefix=url_prefix)
(This is an example not taken from a real code, but you can see the main point: you can register route according to the parameter values and also change how the endpoint responds)
What I do now
Now I dynamically load and register my blueprints with a custom decorator. The following example is not a final proposition, there is still room to improvement.
The custom decorator
import types
from typing import Optional, Callable, Union, cast
def register_route(rule, endpoint: Optional[str] = None, **rule_kwargs):
def my_decorator(func):
final_endpoint = endpoint if endpoint else func.__name__
def wrapper(blueprint, **params):
endpoint_enabled = cast(
Union[Callable[[dict], bool], bool], rule_kwargs.pop("enabled", True)
)
if callable(endpoint_enabled):
endpoint_enabled = endpoint_enabled(**params)
print(endpoint_enabled)
if endpoint_enabled:
blueprint.route(rule, endpoint=final_endpoint, defaults=params, **rule_kwargs)(func)
wrapper.is_url_rule = True
return wrapper
return my_decorator
def is_register_route(func) -> bool:
if not isinstance(func, types.FunctionType):
return False
return isinstance(func, types.FunctionType) and getattr(func, "is_url_rule", False)
The loading mechanism
I start by defining a list of blueprints and their associated parameters.
Here the blueprint defined in the “status” (api.status in fact) module will be registered twice, with two different url_prefix and version param.
As for the “users” blueprint, only its prefix_url is given
blueprints = {
"status": [
{
"name": "status_1",
"kwargs": {"url_prefix": "/api/1.0/status"},
"params": {"version": 1},
},
{
"name": "status_2",
"kwargs": {"url_prefix": "/api/2.0/status"},
"params": {"version": 1.5},
},
],
"users": "/api/1.0/users",
}
I also define a register_blueprints function that will be called with the flask application as first positional parameter. This function will define my root api blueprint.
def register_blueprints(app, **kwargs):
api_blueprint = Blueprint("api", __name__, **kwargs)
for module, blueprints_data in blueprints.items():
# we iterate on blueprints item
module_name = f"api.{module}"
module = __import__(module_name, fromlist=[""])
# we get functions wrapped with our custom decorator
functions = getmembers(module, is_register_route)
if not functions:
continue
if not isinstance(blueprints_data, list):
# we want to work with lists
blueprints_data = [blueprints_data]
for blueprint_data in blueprints_data:
# we determine blueprint parameters
if isinstance(blueprint_data, str):
blueprint_data = {"kwargs": {"url_prefix": blueprint_data}}
blueprint_name = blueprint_data.get(
"name", module.__name__.split(".")[-1]
)
blueprint_kwargs = blueprint_data.get("kwargs", {})
params = blueprint_data.get("params", {})
# we create the blueprint
module_blueprint = Blueprint(blueprint_name, module.__name__)
for register_route in functions:
# we register the endpoints
register_route[1](module_blueprint, **params)
# the "module" blueprint is registered to the root one
api_blueprint.register_blueprint(module_blueprint, **blueprint_kwargs)
app.register_blueprint(api_blueprint, **kwargs)
Registering the endpoints
(this is the api.status blueprint definition module)
from flask import jsonify
from api.blueprint_decorator import register_route
@register_route("/", enabled=lambda **params: params.get("version", 1) > 1)
def status(*, version: int = 1):
return jsonify({"status": "OK", "version": version})
In this example you can see that the route is registered only if the parameter version value is greater than one (conditional registration).
You can also see that inside the endpoint we access the version parameter as a keyword one.
That’s it, now adding a endpoint is quite the same than using a “classic” blueprint.route approach but I can still adapt how endpoints work using parameters defined at the blueprint registration level.
Thanks for reading, feel free to comment !