Skip to content

Mutations with @app.action

Hedron has two common ways to handle unsafe HTTP methods (POST/PUT/PATCH/DELETE). Pick one deliberately — do not mix them on the same form without understanding the difference.

Decision table

You need… Use Why
Classic form POST that returns a full page (redirect or confirmation Page) @app.action("/…") Simplest CSRF-safe mutation; optional fragment_regions if HTMX also targets a region
HTMX POST that swaps a declared region / returns InteractionResult @app.action("/…", fragment_regions=(…,)) Canonical mutation route with a region allowlist
DELETE/PUT with CSRF and a fragment body @app.action(..., method="DELETE", fragment_regions=…) Same allowlist contract

@app.action does accept fragment_regions. Declare the swap host so HTMX HX-Target is authorized (fail-closed 403 otherwise).

Try it (simulated)

HTMX fragment POST — submit swaps the declared result region. Docs simulation.

HTMX on — submit swaps the declared result region.

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

app.py
from __future__ import annotations

import os
from typing import Annotated

from fastapi import Form, Request

from hedron import Hedron, Page, Stack, SubmitButton, Text, TextInput, html
from hedron.security import csrf_token_for_request

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

result = app.region("save-result", description="Save result")


def _csrf(request: Request) -> str:
    return csrf_token_for_request(request, request.app.state.hedron_security)


@app.page("/")
def home(request: Request) -> Page:
    token = _csrf(request)
    return Page(
        Stack(
            html.form(
                html.input(type="hidden", name="csrf_token", value=token),
                TextInput(name="note", value="Ship the docs demo"),
                SubmitButton("Save"),
                method="post",
                **{
                    "hx-post": "/save",
                    "hx-target": result.selector,
                    "hx-swap": "innerHTML",
                },
            ),
            html.div(id=result.id, role="status", aria={"live": "polite"}),
        ),
        title="Mutations",
    )


@app.action("/save", method="POST", fragment_regions=(result,))
def save(note: Annotated[str, Form()] = "") -> object:
    return html.div(html.strong("Saved in region"), Text(note))

Classic form → @action

See the full pasteable sample in Minimal form POST.

@app.action("/save")
def save(request: Request, note: Annotated[str, Form()]) -> Page:
    return Page(Text(f"Saved: {note}"), title="Saved")

HTMX fragment → @app.action POST

See Forms and actions for validation fragments.

from hedron import FragmentRegion, InteractionResult

FORM = FragmentRegion(id="note-form", selector="#note-form")


@app.action("/save", method="POST", fragment_regions=(FORM,))
def save_fragment(...) -> InteractionResult:
    return InteractionResult(content=..., region_id=FORM.id, explanation="...")

CSRF

Both paths require CSRF when security profiles enable it for unsafe methods. Seed the cookie on a safe GET and send csrf_token (form field) or X-CSRF-Token (header).

See also

Action API · Interaction · Hedron methods