Skip to main content

djxi logo HTMX Integration for Django

PyPI PyPI - Python Version PyPI - Django Version CI codecov pre-commit

Full documentation →


HTMX features tend to scatter across urls.py, views.py, and a handful of template snippets. djxi solves this by bundling the URL patterns, view logic, and HTML sections for a feature into a single class — the DXEndpointBattery.

from djxi import DXEndpointBattery, dx_get, dx_post, dx_delete

class TodoBattery(DXEndpointBattery):
    inline_template = """
    <dx-section name="list">
        <ul>{% for item in items %}<li>{{ item }}</li>{% endfor %}</ul>
    </dx-section>
    <dx-section name="form">
        <form hx-post="{% url 'todo:create' %}">...</form>
    </dx-section>
    """

    @dx_get("list", name="list")
    def list(self, request):
        return self.render_section(request, "list", {"items": Item.objects.all()})

    @dx_post("create", name="create")
    def create(self, request):
        Item.objects.create(title=request.POST["title"])
        return self.render_section(request, "list", {"items": Item.objects.all()})

    @dx_delete("item/<int:pk>/delete", name="delete")
    def delete(self, request, pk):
        Item.objects.filter(pk=pk).delete()
        return self.render_empty(request)
# urls.py
urlpatterns = [
    path("todo/", include((TodoBattery.url_patterns(), "todo"), namespace="todo")),
]

Features

Feature Description
DXEndpointBattery Bundles URLs, view logic, and HTML in one class
<dx-section> / <dx-include> Split templates into named, reusable sections
Routing decorators @dx_get, @dx_post, @dx_put, @dx_patch, @dx_delete, @dx_action
battery_prefix Per-battery default URL prefix (overrides global DX_ROUTER_PREFIX)
Async handlers async def methods get ASGI-compatible views; arender_section() for async rendering
Permission hooks requires_auth, login_url, and check_permissions() override
HTMX headers request.htmx / response.htmx with fluent chainable setters
Method override X-HTTP-Method-Override header and <input name="_method"> POST field
Django messages Out-of-band message injection via hx-swap-oob
djxi_routes Management command listing all registered battery routes
djxi.testing DXRequestFactory, DXBatteryTestCase, assertion helpers
Typed Full type annotations; ships py.typed for PEP 561
HTMX 2 & 4 Both HTMX versions supported; switch with DX_HTMX_VERSION

Installation

pip install djxi

settings.py:

INSTALLED_APPS = [
    # ...
    "djxi",
]

MIDDLEWARE = [
    # ... Django middleware ...
    "djxi.middleware.DjxiHeadersMiddleware",  # optional but recommended
]

Base template:

{% load djxi %}
<!doctype html>
<html>
  <head>
    {% htmx_script_inclusion %}
  </head>
  <body {% htmx_headers %}>
    {% flash_messages_inclusion %}
    {% block content %}{% endblock %}
  </body>
</html>

Core Concepts

Sections and Includes

Templates are split using custom HTML tags (no Django template syntax required — they sit alongside it):

<dx-section name="item-form">
  <form hx-post="{% url 'todo:create' %}">
    <input name="title">
    <button>Add</button>
  </form>
</dx-section>

<dx-section name="item-row">
  <li id="item-{{ item.pk }}">
    {{ item.title }}
    <dx-include name="item-actions"/>   {# reuse another section inline #}
  </li>
</dx-section>

<dx-section name="item-actions">
  <button hx-delete="{% url 'todo:delete' item.pk %}">Delete</button>
</dx-section>

Routing

Decorators mark methods as endpoints. url_patterns() converts them to Django paths:

class MyBattery(DXEndpointBattery):
    battery_prefix = "htmx"  # overrides DX_ROUTER_PREFIX for this battery

    @dx_action("item/<int:pk>", methods=["GET", "POST"], name="item")
    def item(self, request, pk):
        ...

    @dx_delete("item/<int:pk>/delete", name="item-delete")
    def delete(self, request, pk):
        ...
urlpatterns = [
    path("api/", include(MyBattery.url_patterns())),
    # → GET/POST  api/htmx/item/<pk>
    # → DELETE    api/htmx/item/<pk>/delete
]

HTMX Headers (Middleware)

@dx_put("item/<int:pk>/flag", name="flag")
def flag(self, request, pk):
    item = Item.objects.get(pk=pk)
    response = self.render_section(request, "item-row", {"item": item})
    # Fluent chaining — all setters return self
    response.htmx.set_trigger("itemFlagged").set_retarget(f"#item-{pk}")
    return response

Async Handlers

@dx_get("items/", name="items")
async def items(self, request):
    items = [item async for item in Item.objects.all()]
    return await self.arender_section(request, "item-list", {"items": items})

Permission / Auth Hooks

class ProtectedBattery(DXEndpointBattery):
    requires_auth = True      # 403 for anonymous users
    login_url = "/login/"     # redirect instead of 403

    # Or fine-grained:
    def check_permissions(self, request):
        if not request.user.has_perm("myapp.can_edit"):
            return HttpResponseForbidden()
        return None  # allow through

Testing

from djxi.testing import DXBatteryTestCase

class TodoBatteryTests(DXBatteryTestCase):
    def test_list_renders_items(self):
        request = self.dx.get("/")          # middleware already applied
        response = TodoBattery().render_section(request, "list", {"items": []})
        self.assert_section_rendered(response, "No todos found")

Configuration

All settings are optional. Override in your settings.py:

Setting Default Description
DX_HTMX_VERSION "4" HTMX version for CDN script tag ("2" or "4")
DX_HTMX_COMPRESSION ".js" Script variant (".js" or ".min.js")
DX_ROUTER_PREFIX "dx" Global URL prefix prepended to all battery routes
DX_MESSAGE_CONTAINER_ID "message-container" DOM ID for OOB message swap target
DX_MESSAGE_SWAP_METHOD "beforeend" hx-swap-oob insertion method
DX_MESSAGE_TEMPLATE "djxi/messages/message_list.html" Template for the message list
DJXI_ROUTE_MODULES ["views","endpoints","batteries"] Modules scanned by djxi_routes

Constants (cannot be overridden via settings.py):

Constant Value
DX_SECTION_TAG "dx-section"
DX_INCLUDE_TAG "dx-include"

Development Status

Pre-Alpha — experimental. API may change between minor versions.

  • v0.2.0: Public Alpha — improve existing facilities / coverage
  • v0.3.0: Public Beta
  • v1.0.0: Stable release

Metadata

Release files for djxi 0.1.9

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

Source distribution (sdist)

Source distribution for djxi 0.1.9
File Size Uploaded Provenance Actions
djxi-0.1.9.tar.gz 45.8 kB With provenance Details for djxi-0.1.9.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for djxi 0.1.9
File Interpreter ABI Platform Actions
djxi-0.1.9-py3-none-any.whl Python 3 none any Details for djxi-0.1.9-py3-none-any.whl

Total release size: 100.4 kB

Release files / djxi-0.1.9.tar.gz

Download URL djxi-0.1.9.tar.gz
Size 45.8 kB
Tags Source
SHA-256 checksum
How to use checksums
d336655f6a357a89d1262ae28514a2f67f3e1d22af729f2292c646722b0de98c
BLAKE2b-256 checksum
How to use checksums
65a6a9ba8a2443c4a4c1417c23aa7c171e9ee7a4ee18f111e02a5c4a99fa3fd9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

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 Jul 27, 2026.

Transparency log

Release files / djxi-0.1.9-py3-none-any.whl

Download URL djxi-0.1.9-py3-none-any.whl
Size 54.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
dc4b42e164a74851758d88a7cff82abb312e2172d051677e44fe34e880db7f3d
BLAKE2b-256 checksum
How to use checksums
fd1924fd9ecabff7b6877d5a12bfcd8989459258ab78eac6ff1fe646e950304e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

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 Jul 27, 2026.

Transparency log

Security for djxi 0.1.9

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.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for djxi-0.1.9.tar.gz

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 Jul 27, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

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