Skip to content

Minimal form POST

Add a CSRF-safe note form to the same FastAPI scaffold from HTMX interactions. The golden path is @app.action plus CsrfField and save.form(), matching Notes + SQLAlchemy. Use this before the advanced region / InteractionResult path in Forms and actions.

FastAPI field name

FastAPI/Flask hidden field is csrf_token via CsrfField(). Django middleware requires csrfmiddlewaretoken and does not accept the portable name.

What you will build

A note field on the home page (next to the notes counter). POST /save appends the note, then redirect_local("/") reloads the page so the count increments. CSRF uses CsrfField() — FastAPI page renders seed the token automatically.

Try it (simulated)

Classic POST — confirmation replaces the notes region. Docs simulation.

Notes saved: 0
Leave a note

Classic POST — save increments the notes count (docs sim).

Minimal runnable app.py that reproduces this demo (real Hedron, not the docs simulator):

app.py
import os
from datetime import datetime, timezone

from fastapi import Form as FastAPIForm

from hedron import (
    CsrfField,
    Form,
    Hedron,
    Page,
    Stack,
    SubmitButton,
    Text,
    TextInput,
    html,
    redirect_local,
)

app = Hedron(
    title="Notes",
    security="standard",
    explorer="off",
    session_secret=os.environ.get("HEDRON_SESSION_SECRET", "dev-only"),
)

_NOTES: list[str] = []


@app.view("/status")
def status():
    stamp = datetime.now(timezone.utc).strftime("%H:%M:%S UTC")
    return html.div(
        Text(f"All systems operational · refreshed {stamp}"),
        role="status",
        aria={"live": "polite"},
    )


@app.view("/notes-count")
def notes():
    return html.div(
        Text(f"Notes saved: {len(_NOTES)}"),
        role="status",
        aria={"live": "polite"},
    )


@app.page("/")
def home() -> Page:
    return Page(
        Stack(
            status(),
            status.refresh_button("Refresh status"),
            notes(),
            notes.refresh_button("Refresh notes count"),
            Text("Leave a note"),
            Form(
                CsrfField(),
                TextInput("note", value="Ship the docs demo", required=True),
                SubmitButton("Save"),
                action="/save",
                method="post",
            ),
        ),
        title="Notes",
    )


@app.action("/save", method="POST")
def save(note: str = FastAPIForm(...)):
    text = note.strip()
    if text:
        _NOTES.append(text)
    return redirect_local("/")

If you used hedron new (or finished the HTMX guide): keep the existing Hedron(...) app, status, _NOTES, and notes. Add the imports and routes below beside what you already have. Do not create a second app file.

1. Add form imports

from fastapi import Form as FastAPIForm

from hedron import (
    CsrfField,
    Form,
    Page,
    Stack,
    SubmitButton,
    Text,
    TextInput,
    html,
    redirect_local,
)

(Merge with imports already present; you only need each name once. Alias FastAPI’s Form so it does not clash with Hedron’s Form component.)

2. Put the form on home() and add /save

Replace your home() from the HTMX guide with this (keep status / notes as you already have them):

@app.page("/")
def home() -> Page:
    return Page(
        Stack(
            Text("Hello from hedron new"),
            status(),
            status.refresh_button("Refresh status"),
            notes(),
            notes.refresh_button("Refresh notes count"),
            Text("Leave a note"),
            Form(
                CsrfField(),
                TextInput("note", value="", required=True),
                SubmitButton("Save"),
                action="/save",
                method="post",
            ),
        ),
        title="Home",
    )


@app.action("/save", method="POST")
def save(note: str = FastAPIForm(...)):
    text = note.strip()
    if text:
        _NOTES.append(text)
    return redirect_local("/")

CsrfField() reads the token from the page RenderContext (seeded when security="standard"). No manual csrf_token_for_request helper is required for this path.

Reload, type a note, click Save. You return to / with Notes saved: 1 (then 2, …). Click Refresh notes count anytime — the fragment shows the same length.

Without a matching CSRF token, the POST returns 403.

Complete file (Path B / reference)

Use this only if you are starting a fresh manual app.py (not extending a scaffold). Copy the Code tab above — it is the same refreshable + form app.

Run it:

uv run uvicorn app:app --reload
uvicorn app:app --reload

Open http://127.0.0.1:8000, submit a note, and confirm the count increments.

Advanced: manual token seeding

Prefer CsrfField() on FastAPI pages. If you are porting an existing app that already calls csrf_token_for_request, you can still pass an explicit token:

from fastapi import Request

from hedron import CsrfField, csrf_token_for_request

def notes(request: Request) -> Page:
    token = csrf_token_for_request(request, request.app.state.hedron_security)
    return Page(
        Form(
            CsrfField(token=token),
            TextInput("note", value="", required=True),
            SubmitButton("Save"),
            action="/save",
            method="post",
        ),
        title="Notes",
    )

Raw html.input(type="hidden", name="csrf_token", value=token) remains valid but is no longer the recommended golden-path pattern — see CSRF composition.

What this teaches

Piece Role
CsrfField() Hidden CSRF input from page RenderContext
GET page Seeds the CSRF cookie / context via the security profile
@app.action(..., method="POST") Mutation route; CSRF validated under security="standard"
_NOTES.append(...) Same in-memory list the HTMX notes region reads
redirect_local("/") Safe local redirect so the full page (and count) refresh
FastAPI Form(...) (aliased) Ordinary request parsing — no Hedron-specific body type

Next steps

  1. Pick a second-hour recipe: Notes + SQLAlchemy or Session auth.
  2. Or continue the golden path: Learning path.
  3. Depth when you need it: Forms and actions · Mutations · Authentication · Security · CSRF composition.