HTMX and Django make status polling trivial

4 min read Original article ↗

I work on a few projects in my spare time, and in one of them, I recently had to implement a simple version of status polling. The main application is built using Django and I don't have a lot of tolerance for working with and maintaining JS build pipelines, so I looked into htmx to solve the problem.

And I'm so thankful that I did that. Here's a quick writeup.

Background

The project in reference is txtcv, a plaintext (JSON resume based) CV building and hosting platform for software developers.

One of the project's features is cover letter generation. Given the contents of your CV and a job description, txtcv is able to generate a cover letter for you. This is implemented as a background job. Basically, the user clicks on a "generate cover letter" button that sends a POST request to the server to create an empty CoverLetter object in the database, that starts a task in the background (using a worker process) that calls out an LLM to populate the contents of the cover letter. As a result of this workflow, the status of the cover letter can go from "pending" to "running", to either "successful" or "failed".

Problem

Given this workflow, it's helpful to let the user know the current status of the cover letter, so they don't need to refresh all the time to know where things stand.

In a typical backend/frontend split, this sort of thing would be implemented using an API returning a JSON representation of the CoverLetter object. The frontend would be set to poll the backend until the status is one of the final values (either successful or failed).

The problem is that neither do I have (or want) an API for this purpose, nor do I use a frontend framework in this project. All the pages in the application are rendered 100% server-side using Django, and I'd like to keep things that way, as long as it's possible.

htmx + Django

I had often read about htmx – a library to add dynamic behavior to webpages without any fuss – but never really had a chance to use it in a project. This problem felt like a perfect fit for htmx.

So, I installed django-htmx in my project, added a {% htmx_script %} to the base Django template, and got to work.

Status polling with htmx and Django

It turns out that the workflow for implementing status polling using htmx and Django is super simple. While there are multiple ways to do it, this section describes just one.

I added a Django view function in the backend that renders a cover letter template partial (all server side), given a cover letter ID. The Python code looks roughly as follows:

from django.shortcuts import get_object_or_404, render

from app.models import CoverLetter

def render_cover_letter(request, id: UUID):
    cover_letter = get_object_or_404(CoverLetter, id=id)
    
    return render(
        request, "cover-letter.html", {"cover_letter": cover_letter}
    )

Fairly simple. We get a cover letter ID from the URL, render the Django/HTML template, and send it back as a response to the browser.

The cover-letter.html template is where the real magic happens, so let's have a look at that.

<div
    {% if not cover_letter.has_final_status %}
    hx-trigger="every 2s"
    hx-get="{% url 'cover-letter' id=cover_letter.id %}"
    hx-target="this"
    hx-swap="outerHTML"
    {% endif %}
    >
    <div class="flex items-start space-x-4 p-6 bg-base-100 rounded-lg border border-base-300">
        ...
    </div>
</div>

The code inside the outermost <div> is focused on rendering the cover letter card UI, which is not important for the purpose of this blog post. The real magic happens through the conditional hx- attributes added to that outer <div>. Basically, we're telling the browser that if the status of the cover letter is not one of the final values, then fetch the /cover-letter/<id> URL every 2 seconds and replace the <div> with the response from the server. And if the cover letter has a successful or failed status, then all those hx- attributes are not even rendered in the Django template. All of this logic is controlled server side, without a single line of frontend JavaScript.

Another neat thing is that whatever the "final" status of the cover letter is, the frontend doesn't need to know or care. At the moment it's abstracted into a @property on the CoverLetter Django model, so external code can just use that property instead of having to know what exactly those final status values are.

Conclusion

And there we have it, status polling made trivial through htmx and Django, without a single line of frontend JS code or build pipelines.

No API endpoints to maintain, no frontend framework to set up, no JavaScript to write. Just a Django view that renders a template, a few htmx attributes, and the browser handles the rest. What could have been a complex feature turned out to be super straightforward, thanks to htmx.