Flask API Fundamentals · Structure and Error Handling
Blueprints for API Modules
Split a growing Flask API into blueprints, register them in an app factory with URL prefixes, and avoid circular imports.
A Flask app can start as one file, but by the time it has users, orders and billing endpoints that file is hundreds of lines long and everyone edits it at once. Blueprints let you split routes into modules that each own one area of the API, then plug them into the app with a URL prefix.
A blueprint is a group of routes
You create a Blueprint the same way you create an app, and add routes to it with the same decorators:
# app/users.py
from flask import Blueprint, jsonify
users = Blueprint("users", __name__, url_prefix="/users")
USERS = {1: "Ada", 2: "Linus"}
@users.get("/")
def list_users():
return jsonify([{"id": i, "name": n} for i, n in USERS.items()])
@users.get("/<int:user_id>")
def get_user(user_id):
if user_id not in USERS:
return jsonify(error="user not found"), 404
return jsonify(id=user_id, name=USERS[user_id])
Nothing is served yet. A blueprint only records its routes; they become real when you register it on an app.
Register blueprints in an app factory
The usual pattern is a create_app() function that builds the app, loads its settings and registers every blueprint. Tests can then create a fresh app for each run.
# app/__init__.py
from flask import Flask
def create_app(config=None):
app = Flask(__name__)
app.config.update(config or {})
from .users import users
from .orders import orders
app.register_blueprint(users, url_prefix="/api/v1/users")
app.register_blueprint(orders, url_prefix="/api/v1/orders")
return app
A url_prefix passed to register_blueprint replaces the one set on the blueprint. That makes it easy to mount the same module under /api/v1 today and /api/v2 later.
The final URL is the prefix plus the route. You can see the rule without running Flask:
def mount(prefix, routes):
return [(method, prefix.rstrip("/") + path) for method, path in routes]
users = [("GET", "/"), ("GET", "/<int:user_id>"), ("POST", "/")]
orders = [("GET", "/"), ("POST", "/<int:order_id>/cancel")]
table = mount("/api/v1/users", users) + mount("/api/v1/orders", orders)
for method, url in table:
print(f"{method:5} {url}")
Output
GET /api/v1/users/ GET /api/v1/users/<int:user_id> POST /api/v1/users/ GET /api/v1/orders/ POST /api/v1/orders/<int:order_id>/cancel
Endpoint names include the blueprint
Inside a blueprint, each route's endpoint name is prefixed with the blueprint's name. Use that name with url_for, for example to set a Location header after creating something:
from flask import url_for
@users.post("/")
def create_user():
new_id = 3
body = jsonify(id=new_id)
return body, 201, {"Location": url_for("users.get_user", user_id=new_id)}
Inside the same blueprint you can also write the short form url_for(".get_user", ...).
Hooks and error handlers per blueprint
A blueprint can have its own before_request hook and error handlers. They run only for that blueprint's routes, which is handy when, say, only the admin API needs an extra check:
from flask import Blueprint, abort, request
admin = Blueprint("admin", __name__)
@admin.before_request
def require_admin_key():
if request.headers.get("X-Admin-Key") != "change-me":
abort(403)
One exception: a blueprint's 404 and 405 handlers don't catch URLs that match no route at all, because Flask can't tell which blueprint such a URL belongs to. Register those handlers on the app (see the next lesson).
How to split an API
Group routes by the thing they manage, not by HTTP method or by file type:
app/
__init__.py create_app() registers the blueprints
users.py /api/v1/users
orders.py /api/v1/orders
billing.py /api/v1/billing
errors.py app-wide JSON error handlers
Common mistakes
- Two blueprints with the same name. Blueprint names must be unique within an app, because they prefix endpoint names. Flask raises an error when a second blueprint reuses a name.
- Circular imports. If
users.pyimportsappfromapp/__init__.pyand that file importsusers.py, Python fails. Import the blueprints insidecreate_app(), as above, and usecurrent_appinside handlers instead of a globalapp. - Forgetting to register. A route defined on a blueprint that was never registered returns
404. Runflask routesto list every URL the app really serves.
Try it
Create an orders blueprint with GET / and GET /<int:order_id>, register it under /api/v1/orders, and check with flask routes that both URLs appear.
Open in the app to mark lessons complete, take notes and track your progress.