Skip to content

Applications

Use the JinjaMiddleware middleware and TemplateRouter ASGI app to add template support to Starlette applications.

Project Structure

A typical Starlette-Templates application is organized as a Python package:

myapp/
├── __init__.py
├── app.py                 # Main application file
├── templates/             # Template files
│   ├── base.html          # Base layout
│   ├── index.html         # Homepage
│   ├── about.html         # About page
│   ├── 404.html           # Custom 404 page
│   ├── 500.html           # Custom 500 page
│   ├── partials/          # Reusable fragments
│   │   ├── navigation.html
│   │   └── footer.html
│   └── components/        # Component templates
│       ├── alert.html
│       └── card.html
└── static/                # Static files
    ├── css/
    ├── js/
    └── images/

Your app.py would typically look like:

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.routing import Mount, Route
from starlette.responses import JSONResponse
from jinja2 import PackageLoader
from importlib.resources import files

from starlette_templates.routing import TemplateRouter
from starlette_templates.staticfiles import StaticFiles
from starlette_templates.middleware import JinjaMiddleware

PKG_DIR = files("myapp")

async def api(request):
    return JSONResponse({"message": "API Root"})

app = Starlette(
    routes=[
        # Custom routes first
        Mount("/api", api, name="api"),
        # Static files
        Mount("/static", StaticFiles(directories=[PKG_DIR / "static"]), name="static"),
        # Template files for remaining routes
        Mount("/", TemplateRouter()),
    ],
    middleware=[
        Middleware(
            JinjaMiddleware,
            template_loaders=[PackageLoader("myapp", "templates")],
        )
    ]
)

Basic Setup

A minimal Starlette Templates application requires two components:

  1. JinjaMiddleware - Middleware that configures the Jinja2 environment, making it available on request state
  2. TemplateRouter - An ASGI app that serves HTML templates
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.routing import Mount
from jinja2 import PackageLoader

from starlette_templates.routing import TemplateRouter
from starlette_templates.middleware import JinjaMiddleware

app = Starlette(
    routes=[
        Mount("/", TemplateRouter()),
    ],
    middleware=[
        Middleware(
            JinjaMiddleware,
            template_loaders=[PackageLoader("mypackage", "templates")],
        )
    ]
)

Static Files

Starlette Templates provides enhanced static file handling with gzip compression and multi-directory support using StaticFiles.

from starlette.routing import Mount
from starlette_templates.staticfiles import StaticFiles

app = Starlette(
    routes=[
        Mount("/static", StaticFiles(directories=["static"]), name="static"),
        Mount("/", TemplateRouter()),
    ]
)

For detailed information about static file handling, including pre-compressed files, multi-directory support, and HTTP caching, see the Static Files guide.

Custom Routes

Override the default template routing by defining custom routes. Routes defined before the TemplateRouter mount take precedence:

from starlette.routing import Route
from starlette.requests import Request
from starlette_templates.responses import TemplateResponse

async def homepage(request: Request) -> TemplateResponse:
    return TemplateResponse("home.html", context={"title": "Welcome"})

app = Starlette(
    routes=[
        # Custom route for homepage
        Route("/", homepage, name="home"),
        # Template routing for all other routes
        Mount("/", TemplateRouter())
    ],
    middleware=[
        Middleware(
            JinjaMiddleware,
            template_loaders=[PackageLoader("myapp", "templates")]
        )
    ]
)

Middleware Configuration

JinjaMiddleware

JinjaMiddleware configures and injects the Jinja2 environment into request state, making it available at request.state.jinja_env.

from jinja2 import PackageLoader, FileSystemLoader
from starlette_templates.middleware import JinjaMiddleware

app = Starlette(
    middleware=[
        Middleware(
            JinjaMiddleware,
            template_loaders=[
                PackageLoader("myapp", "templates"),
                FileSystemLoader("custom/templates"),
            ],
            include_default_loader=True,  # Include built-in templates
        )
    ]
)

The middleware provides:

  • Automatic template loader configuration
  • Built-in Jinja2 globals: url_for, url, absurl, jsonify
  • Request state injection for all templates

Registering Components

Register ComponentModel classes to make them available in all templates:

from starlette_templates.components.base import ComponentModel

class Alert(ComponentModel):
    template: str = "components/alert.html"
    message: str
    variant: str = "info"

app = Starlette(
    middleware=[
        Middleware(
            JinjaMiddleware,
            template_loaders=[PackageLoader("myapp", "templates")],
            extra_components=[Alert],  # Register component
        )
    ]
)

Then use directly in templates:

{{ Alert(message="System error", variant="danger") }}

Context Processors

Add custom context variables to all templates using context processors:

from starlette.requests import Request

async def add_user_context(request: Request) -> dict:
    return {"user": await get_current_user(request)}

async def add_site_context(request: Request) -> dict:
    return {
        "site_name": "My Site",
        "year": datetime.now().year,
    }

templates = TemplateRouter(
    context_processors=[add_user_context, add_site_context]
)

Context processors are async functions that receive the Request object and return a dictionary of context variables.

Route-Specific Context Processors

Context processors can also be applied to specific routes using [Route][starlette.routing.Route] objects with path patterns:

from starlette.requests import Request
from starlette.routing import Route

async def add_product_context(request: Request) -> dict:
    """Context processor that only runs for product pages."""
    product_id = request.path_params.get("product_id")
    return {"product": await get_product(product_id)}

async def add_user_context(request: Request) -> dict:
    """Global context processor that runs for all templates."""
    return {"user": await get_current_user(request)}

templates = TemplateRouter(
    context_processors=[
        add_user_context,  # Runs for all templates
        # Only runs when path matches /products/{product_id}
        Route("/products/{product_id}", add_product_context),
    ]
)

Route-specific context processors only execute when the URL path matches the route pattern, allowing you to add context data conditionally based on the request path. Path parameters from the route (like {product_id}) are available via request.path_params.

Error Handling

Starlette Templates provides error handling with custom exceptions, error pages, and JSON:API error responses.

Create custom error pages by adding templates like 404.html, 500.html, or error.html to your template directory. The framework automatically serves these pages for browser requests and returns JSON:API compliant responses for API requests.

For detailed information about error handling, custom exceptions, error pages, and JSON:API responses, see the Error Handling guide.

HTTP Caching

Configure HTTP caching with ETag and Last-Modified headers on TemplateRouter:

templates = TemplateRouter(
    cache_max_age=3600,      # Cache for 1 hour
    gzip_min_size=1024,      # Compress files > 1KB
)

TemplateRouter automatically:

  • Generates ETag headers based on file modification time
  • Sets Last-Modified headers
  • Returns 304 Not Modified responses when appropriate
  • Compresses responses over the threshold with gzip