Contacts

Plain htmx CRUD with validation, OOB swaps, and response headers

Page actions AI-ready formats and sharing
Open LLM text
Share with AI
Ask Claude Ask ChatGPT Ask Gemini Ask Copilot

A no-dependency htmx CRUD app — add, search, edit, and delete contacts — built on plain Chirp return types and in-memory state. Reach for it after your first fragment app when you want a realistic mutation loop: a search box that swaps a table, a form that re-renders with 422 validation errors, an add that updates the table and its count badge in one response, and a delete that re-renders the table and fires anHX-Trigger event to drive a toast.

What It Teaches

  • [[docs/about/core-concepts/return-values|Page]] for full-page vs. fragment negotiation
  • [[docs/build-apps/html-fragments/fragments|Fragment]] for search and delete updates
  • [[docs/build-apps/forms-data/forms-validation|ValidationError]] for 422 form re-renders
  • [[docs/quality/contracts-debugging/oob-registry|OOB]] for updating the table and count in one response
  • anHX-Triggerevent that drives a toast after delete
  • frozen dataclasses plus a lock for thread-safe in-memory state

Run It

PYTHONPATH=src python examples/standalone/contacts/app.py
pytest examples/standalone/contacts/

Then openhttp://127.0.0.1:8000/.

One Response, Two Targets

Adding a contact updates two regions in a single response: the table re-renders, and the heading's count badge swaps out of band. The handler returns oneOOB wrapping twoFragments.

@app.route("/contacts", methods=["POST"], name="contacts.add")
async def add_contact(request: Request):
    form = await request.form()
    result = validate(form, _CONTACT_RULES)
    if not result:
        return ValidationError(
            "contacts.html", "contact_form",
            retarget="#form-section",
            errors=result.errors,
            form={"name": form.get("name", ""), "email": form.get("email", "")},
        )

    _add_contact(form.get("name", ""), form.get("email", ""))
    contacts = _get_contacts()
    return OOB(
        Fragment("contacts.html", "contact_table", contacts=contacts),
        Fragment("contacts.html", "contact_count", target="contact-count", count=len(contacts)),
    )

Source: examples/standalone/contacts/app.py.

Source

Next