Skip to content

Templating

Not every hunt ends in JSON. Sometimes the raptor needs to leave a polished footprint for browsers and humans: an HTML page, a form, a dashboard. VelociPy’s templating layer keeps that footprint light and safe, delegating rendering to Jinja2 while wrapping the result in a clean HTMLResponse.

Templates are optional. Import them only when your terrain calls for HTML. When you do, autoescaping is enabled by default, so user content is treated as suspect until the template says otherwise.

Installation

Jinja2 is not installed with the core framework. Add it with the jinja2 extra:

pip install "velocipy[jinja2]"

Rendering a page

Create a templates/ directory next to your application and drop an index.html inside it.

<!-- templates/index.html -->
<h1>Hello, {{ name }}!</h1>

Point a Templates instance at that directory, then return TemplateResponse from a route. The request object must always be passed in the context so templates can resolve URL helpers if you add them later.

from velocipy import Request, VelociPy
from velocipy.templating import Templates

app = VelociPy()
templates = Templates(directory="templates")


@app.get("/")
async def index(request: Request):
    return templates.TemplateResponse(
        "index.html",
        {"request": request, "name": "World"},
    )

Tip

Templates loads files relative to the directory you give it. Use an absolute Path if your working directory shifts between test and production.

Feeding validated data into templates

Raw dictionaries work, but a raptor prefers a clean kill. Pass validated models into the context so your templates receive typed, predictable data.

from typing import Annotated
from pydantic import BaseModel
from velocipy import Query, Request, VelociPy
from velocipy.templating import Templates

app = VelociPy()
templates = Templates(directory="templates")


class Item(BaseModel):
    name: str
    price: float


@app.get("/items/{item_id}")
async def item_detail(
    request: Request,
    item_id: int,
    currency: Annotated[str, Query()] = "USD",
):
    item = Item(name="Hoodie", price=49.99)
    return templates.TemplateResponse(
        "item.html",
        {
            "request": request,
            "item": item,
            "currency": currency,
        },
    )
<!-- templates/item.html -->
<h1>{{ item.name }}</h1>
<p>{{ currency }} {{ item.price }}</p>

VelociPy validates item_id and currency before the handler runs, so the template only sees clean prey.

Status codes, headers, and context defaults

TemplateResponse accepts status_code and headers for times when the default 200 OK is not enough.

@app.get("/missing")
async def missing(request: Request):
    return templates.TemplateResponse(
        "404.html",
        {"request": request, "path": request.path},
        status_code=404,
        headers={"cache-control": "no-store"},
    )

Advanced patterns

Custom Jinja2 environment

The default Templates object sets autoescape=True and uses a filesystem loader. If you need custom filters, globals, or a different loader, create a standard jinja2.Environment and keep the response wrapper thin:

import jinja2
from velocipy.http.responses import HTMLResponse

env = jinja2.Environment(
    loader=jinja2.FileSystemLoader("templates"),
    autoescape=True,
)
env.filters["currency"] = lambda value, symbol="$": f"{symbol}{value:.2f}"


def render(name: str, context: dict) -> HTMLResponse:
    return HTMLResponse(env.get_template(name).render(context))

Shared template context with dependencies

When many routes repeat the same context keys, move them into a dependency.

from typing import Annotated
from velocipy import Depends, Request


async def base_context(request: Request):
    return {
        "request": request,
        "site_name": "VelociPy Nest",
    }


@app.get("/dashboard")
async def dashboard(
    request: Request,
    ctx: Annotated[dict, Depends(base_context)],
):
    ctx["user"] = "alpha"
    return templates.TemplateResponse("dashboard.html", ctx)

Warning

BackgroundTasks is not supported inside dependencies. Keep template preparation synchronous or async-within the handler, and defer side effects to a BackgroundTasks parameter on the route.

At a glance

Gear What it does
Templates(directory) Loads Jinja2 templates from a directory with autoescaping on.
TemplateResponse(name, context, status_code=200, headers=None) Renders a template and returns an HTMLResponse.
{"request": request, ...} Always include the request in the context.
jinja2.Environment Drop down for custom filters, globals, or loaders.
pip install "velocipy[jinja2]" Installs the optional Jinja2 dependency.

See examples/templates.py for a runnable demo.