Skip to main content

fastapi-hotwire

PyPI Python CI License: MIT

fastapi-hotwire brings Hotwire's Turbo protocol to FastAPI. Render targeted DOM updates with <turbo-stream> responses, return individual Jinja blocks for <turbo-frame> requests, queue session-backed flash messages, and validate forms in place — all from your existing FastAPI handlers, with no JSON layer or client-side framework. Ships with pytest helpers and a Protocol-based design so you can swap template engines or session backends.

Install

pip install fastapi-hotwire
# or
uv add fastapi-hotwire

For the Pydantic-backed validation-error stream:

pip install "fastapi-hotwire[forms]"

Quickstart

from fastapi import FastAPI, Form, Request
from fastapi_hotwire import HotwireTemplates, TurboStreamResponse, streams

app = FastAPI()
templates = HotwireTemplates(directory="templates", flashes=False)
todos: list[dict] = []


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


@app.post("/todos")
def create(request: Request, text: str = Form(...)):
    todo = {"id": len(todos) + 1, "text": text}
    todos.append(todo)
    return templates.render_stream(
        request, "index.html", "todo_row",
        action="append", target="todos", todo=todo,
    )


@app.post("/todos/{todo_id}/delete")
def delete(todo_id: int):
    todos[:] = [t for t in todos if t["id"] != todo_id]
    return TurboStreamResponse(streams.remove(target=f"todo-{todo_id}"))

A complete runnable version of this example lives in examples/minimal/.

What's in the box

Module What it does
TurboStreamResponse A Response subclass with Content-Type: text/vnd.turbo-stream.html.
streams Pure-function builders for <turbo-stream> actions (append, prepend, replace, update, remove, before, after, refresh).
TurboContext A FastAPI dependency that summarizes how the current request relates to Turbo (frame? stream? top-level visit?).
HotwireTemplates A Jinja2Templates wrapper that adds render_block(...) and render_stream(...), plus an automatic flashes context processor.
flash / get_flashed Session-backed flash messages with both a redirect-style and a Hotwire-native turbo-stream flow.
forms A Pydantic ValidationError → turbo-stream renderer for in-place form validation.
testing pytest assertions and request helpers (assert_turbo_stream, parse_streams, assert_turbo_frame, turbo_frame_request, turbo_stream_request).

TurboStreamResponse

from fastapi_hotwire import TurboStreamResponse, streams

@app.post("/items")
def create():
    return TurboStreamResponse([
        streams.append(item_html, target="items"),
        streams.update(counter_html, target="item-count"),
    ])

Pass a single string, a list of strings, or None. The class also works as response_class=TurboStreamResponse so OpenAPI documents the media type.

streams

Each builder returns a markupsafe.Markup so it composes safely with Jinja templates:

from fastapi_hotwire import streams

streams.append("<li>...</li>", target="todos")
streams.replace(form_html, target="contact-form")
streams.remove(target="todo-42")
streams.refresh()  # Turbo 8 page-refresh

The html argument is interpolated verbatim into the <template> envelope. It must be safe HTML (Jinja autoescaped output is safe). Attribute values (target=, targets=) are HTML-escaped automatically.

TurboContext

from typing import Annotated
from fastapi import Depends
from fastapi_hotwire import TurboContext, turbo_context

@app.post("/items")
async def create(turbo: Annotated[TurboContext, Depends(turbo_context)]):
    if turbo.is_frame:
        return frame_response(...)
    if turbo.accepts_stream:
        return stream_response(...)
    return full_page_response(...)

Fields: is_frame, frame_id, accepts_stream, is_visit.

HotwireTemplates

from fastapi_hotwire import HotwireTemplates

templates = HotwireTemplates(directory="templates")

@app.get("/items/{id}")
def item_frame(request: Request, id: int):
    # Render only the {% block item %} of items.html — useful for
    # responding to a <turbo-frame src="..."> request.
    return templates.render_block(request, "items.html", "item", item=load(id))

@app.post("/items")
def create(request: Request, text: str = Form(...)):
    item = save(text)
    # Render the {% block item %} as a turbo-stream that appends to #items.
    return templates.render_stream(
        request, "items.html", "item",
        action="append", target="items", item=item,
    )

The flashes context processor is registered automatically; pass flashes=False to opt out.

flash

from fastapi_hotwire import flash, get_flashed

# 1. Classic post-redirect-get flow:
@app.post("/save")
def save(request: Request):
    flash(request, "Saved", category="success")
    return RedirectResponse("/", status_code=303)

# 2. Hotwire-native: respond with a stream that appends to #flash without redirecting:
@app.post("/save")
def save(request: Request):
    return flash.stream(request, "Saved", category="success")

Templates rendered through HotwireTemplates automatically receive the queued flashes list.

A complete runnable example lives in examples/flash/.

forms

from fastapi_hotwire.forms import validation_error_stream

# Render Pydantic validation errors as a turbo-stream that replaces
# only the form's block — no full-page reload, no scroll loss.
try:
    Contact.model_validate(form_data)
except ValidationError as exc:
    return validation_error_stream(
        exc, templates=templates, template="contact.html",
        block="form", target="contact-form", request=request,
    )

testing

from fastapi_hotwire.testing import (
    assert_turbo_stream, parse_streams, assert_turbo_frame,
    turbo_frame_request, turbo_stream_request,
)

def test_create_appends_a_row(client):
    resp = client.post("/todos", data={"text": "ship"})
    assert_turbo_stream(resp)
    actions = parse_streams(resp)
    assert actions[0].action == "append"
    assert actions[0].target == "todos"

Pluggability

fastapi_hotwire.protocols defines the integration seams:

  • TemplateRenderer — anything implementing render(name, context) -> str.
  • BlockRenderer — anything implementing render_block(name, block, context) -> str. The default Jinja2BlockRenderer is one implementation.
  • SessionLike — any MutableMapping[str, Any] that hangs off request.session (Starlette SessionMiddleware, starsessions, an in-memory dict).

You don't need to use the bundled Jinja2 / Starlette code paths to use this library.

Examples

Two full runnable examples live under examples/:

  • examples/minimal/ — A todo list with turbo-stream append + remove. The simplest possible integration.
  • examples/flash/ — Session-backed flash messages, with both a PRG and a Hotwire-native flow.

Run either with uv run uvicorn app:app --reload from inside the example directory.

Non-goals

fastapi-hotwire deliberately does not:

  • Push to clients via WebSocket / SSE — Hotwire's broadcast / turbo_stream_from patterns belong in app code with your message bus of choice.
  • Bundle a Stimulus JavaScript distribution — load Stimulus the way you load any other JS.
  • Inject logging / observability / tracing — those are application concerns, not library concerns.
  • Replace request.url_for(...) with anything more magical.

This list will not grow.

Contributing

See CONTRIBUTING.md. Issues and PRs welcome — please file an issue first for anything bigger than a typo so we can align on scope.

This project follows the Contributor Covenant Code of Conduct.

License

MIT © 2026 Pyre

Metadata

Release files for fastapi-hotwire 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fastapi-hotwire 0.3.0
File Size Uploaded
fastapi_hotwire-0.3.0.tar.gz 62.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for fastapi-hotwire 0.3.0
File Interpreter ABI Platform
fastapi_hotwire-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 81.8 kB

Release files / fastapi_hotwire-0.3.0.tar.gz

Download URL fastapi_hotwire-0.3.0.tar.gz
Size 62.9 kB
Tags Source
SHA-256 checksum
How to use checksums
90f0b5dd2c5f7598f9975e800f59a581a07c2705b1f81f1126afdb3d972d901a
BLAKE2b-256 checksum
How to use checksums
88ebf9a5ba4047a04b4c43f300c5ef1cd862bf787d63802c650dc4e8aef82366
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 15, 2026.

Transparency log

Release files / fastapi_hotwire-0.3.0-py3-none-any.whl

Download URL fastapi_hotwire-0.3.0-py3-none-any.whl
Size 18.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
87b65b7dc7654b63c67f3eded03403b5811a4abbf7b2ad73ddc01a084b153e3f
BLAKE2b-256 checksum
How to use checksums
cbf9e4b74ecc453751c78c4576320804775ab350477eb594cefd9c4cce0bd13f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 15, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page