Starlette & FastAPI Integration

Add a typed Kida component and fragment endpoint to FastAPI in ten minutes

3 min read 537 words

Use Kida's Starlette adapter from FastAPI to render a typed component, a form, and a named fragment. Kida requires Python 3.14 or newer.

Coming from Jinja2? Read Coming from Jinja2. Kida's{% set %}is block-scoped and does not leak values out of loops or other blocks.

FastAPI in ten minutes

1. Install

uv add fastapi uvicorn kida-templates

2. Configure Kida and add two endpoints

from urllib.parse import parse_qs

from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse

from kida.contrib.starlette import KidaTemplates

app = FastAPI()
templates = KidaTemplates(directory="templates")


@app.get("/", response_class=HTMLResponse)
async def home(request: Request) -> HTMLResponse:
    return templates.TemplateResponse(
        request,
        "components.html",
        {"title": "First component", "summary": "Edit me"},
    )


@app.post("/preview", response_class=HTMLResponse)
async def preview(request: Request) -> HTMLResponse:
    form = parse_qs((await request.body()).decode())
    template = templates.get_template("components.html")
    html = template.render_block(
        "preview",
        title=form.get("title", [""])[0],
        summary=form.get("summary", [""])[0],
    )
    return HTMLResponse(html)

Parsing this small URL-encoded form directly keeps python-multipartout of the example. Applications that already use FastAPI's form dependency can use Form()parameters instead.

3. Create the typed component

Save this astemplates/components.html:

{% def panel(title: str) %}
<section class="panel">
  <h1>{{ title }}</h1>
  {% slot %}
</section>
{% enddef %}

{% def text_field(name: str, label: str, value: str = "") %}
<label>
  {{ label }}
  <input name="{{ name }}" value="{{ value }}">
</label>
{% enddef %}

{% block form %}
{% call panel("Component form") %}
<form method="post" action="/preview">
  {{ text_field("title", "Title", title) }}
  {{ text_field("summary", "Summary", summary) }}
  <button type="submit">Preview</button>
</form>
{% endcall %}
{% endblock %}

{% block preview %}
<article id="preview">
  <h2>{{ title }}</h2>
  <p>{{ summary }}</p>
</article>
{% endblock %}

Kida checks each component call against its typed defsignature. The full endpoint and fragment endpoint share compilation and autoescaping; the latter returns onlypreviewfor HTMX, Turbo, or another HTML-over-the-wire client.

4. Run it

uv run uvicorn app:app --reload

The repository includes a runnable, smoke-tested version at examples/fastapi_components.

Use the adapter with Starlette

The same adapter works directly with Starlette:

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

from kida.contrib.starlette import KidaTemplates

templates = KidaTemplates(directory="templates")


async def home(request: Request):
    return templates.TemplateResponse(
        request,
        "components.html",
        {"title": "First component", "summary": "Edit me"},
    )


app = Starlette(routes=[Route("/", home)])

KidaTemplates accepts either directory=, or a preconfigured env=. It can also acceptcontext_processors, whose callables receive the request and return context mappings.TemplateResponse()adds the request to context and supportsstatus_code, headers, and media_type.

Async and streaming templates

TemplateResponse()renders ordinary templates synchronously. For templates that contain{% async for %} or {{ await }}, use Kida's async stream API:

from starlette.responses import StreamingResponse


@app.get("/feed")
async def feed(request: Request):
    template = templates.get_template("feed.html")

    async def chunks():
        async for chunk in template.render_stream_async(
            items=await get_items(),
            request=request,
        ):
            yield chunk

    return StreamingResponse(chunks(), media_type="text/html")

Templates can use {% flush %}to choose stream boundaries. For a single async block, userender_block_stream_async().

HTMX request metadata

TemplateResponse() records HX-Request, HX-Target, HX-Trigger, and HX-Boosted in Kida's render context as hx_request, hx_target, hx_trigger, and hx_boosted. Framework code can combine that metadata with render_block()while keeping render state request-local.

Next steps