Serving Dynamic Pages and Organizing Routes with Sanic Blueprints
Sanic processes incoming HTTP requests by mapping Uniform Resource Identifiers (URIs) to asynchronous view functions. When a client connects, the framework matches the request path against registered routes, executes the corresponding handler, and returns an HTTP response object.
Routing Mechanics and View Handlers
The @app.route() decorator binds a specific URI pattern to an async callable. Inside the application lifecycle, this registration stores the handler reference alongside HTTP method constraints in an internal route table. Upon receiving a request, the server extracts the URL path, queries the route registry, invokes the matched function with the Request object as an argument, and passes the resulting response back to the underlying ASGI/HTTP server.
from sanic import Sanic
from sanic.response import text
app = Sanic("core_engine")
@app.route("/status")
async def check_health(request):
return text({"status": "operational"})
In this structure, check_health acts as the bridge between network input and application logic. It decouples incoming traffic from business processing, allowing clean separation of concerns.
Project Organization Conventions
Maintaining a growing number of endpoints requires deliberate directory structuring. Standard practices include:
- Placing static assets (CSS, JavaScript, images) under a dedicated
assetsorstaticroot. - Storing template files in a
templatesdirectory. - Grouping route handlers within a
routesorcontrollerspackage.
As applications scale, flat routing configurations become difficult to manage. Conflicts arise when multiple developers register overlapping paths, and static/template file distribution becomes chaotic.
Modular Routing with Blueprints
Blueprints solve scalability issues by encapsulating routes, template directories, and static asset mappings into reusable modules. Each blueprint operates with an isolated namespace, supports custom URL prefixes, and reduces naming collisions across large codebases.
Consider a service split into two functional areas: a document viewer for rendered HTML and a data exchange layer returning JSON payloads. Using Blueprints, these sections can be developed independently while sharing a single entry point.
Recommended Directory Layout
project_root/
├── app_entry.py
├── assets/
│ ├── doc_viewer/
│ │ ├── css/
│ │ └── js/
│ └── data_portal/
│ ├── css/
│ └── js/
├── templates/
│ ├── doc_viewer/
│ │ └── base.html
│ └── data_portal/
│ └── manifest.html
└── routes/
├── __init__.py
├── html_router.py
└── json_router.py
HTML Viewer Blueprint
This module handles template rendering using Jinja2 with asynchronous support.
# routes/html_router.py
import sys
from jinja2 import Environment, PackageLoader, select_autoescape
from sanic import Blueprint
from sanic.response import html
enable_async = sys.version_info >= (3, 6)
doc_bp = Blueprint("document_view", url_prefix="/docs")
# Configure static file serving
pkg_name = "routes"
pkg_path = doc_blueprint.static(
"/docs/static",
"./assets/doc_viewer"
)
env = Environment(
loader=PackageLoader(pkg_name, "../templates/doc_viewer"),
autoescape=select_autoescape(["html", "xml"]),
enable_async=enable_async
)
async def render_page(tpl_name, context=None):
ctx = context or {}
tmpl = env.get_template(tpl_name)
return html(await tmpl.render_async(**ctx))
@doc_bp.route("/")
async def show_dashboard(request):
return await render_page("base.html", {"section": "main"})
@doc_bp.route("/archive")
async def list_documents(request):
sample_docs = [
{"id": "A1", "title": "System Overview", "href": "#/view/A1"}
]
return await render_page("base.html", {"items": sample_docs})
JSON Data Blueprint
This module exposes structured data without template rendering.
# routes/json_router.py
from sanic import Blueprint
from sanic.response import json
data_bp = Blueprint("data_exchange", url_prefix="/api")
@data_bp.route("/feed/entries")
async def get_feed_items(request):
raw_records = [
{
"title_detail": {"value": "Scaling Asynchronous Workloads"},
"link": "https://example.com/scale"
}
]
transformed_payload = [
{"headline": rec["title_detail"]["value"], "target_url": rec["link"]}
for rec in raw_records
]
return json(transformed_payload)
Route Initialization
Aggregate all blueprints in the application factory or startup script to ensure they are registered before the server binds to ports.
# routes/__init__.py
from .html_router import doc_bp
from .json_router import data_bp
# app_entry.py
import os
from sanic import Sanic
from src.routes import doc_bp, data_bp
app = Sanic("unified_gateway")
app.register_blueprint(doc_bp)
app.register_blueprint(data_bp)
# Serve global static files
app.static("/resources", os.path.join(os.getcwd(), "assets"))
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, debug=True)
Using this modular approach eliminates prefix duplication and isolates resource dependencies. Each blueprint maintains its own template roots and static directories, preventing cross-contamination during development. Testing the implementation involves targeting the prefixed endpoints dircetly:
GET http://localhost:8080/docs/
GET http://localhost:8080/docs/archive
GET http://localhost:8080/api/feed/entries
Accessing these endpoints verifies that each blueprint operates independently while sharing the same application instance.