In this post, I’ll walk you through my experiences building the TodoMVC app using HTMX. I'll cover the architectural considerations, handy tips, pros and cons, insights, and everything in between.
The code can be found here, along with an elaborated README - https://github.com/mbarzeev/todomvc/tree/htmx/examples/htmx
The TodoMVC project
Back when JavaScript frameworks were flooding the web ecosystem and a new, promising framework seemed to emerge every month as the next big thing, Addy Osmani set out to create a benchmark application. His goal was to push each framework to its reasonable limits, giving developers a sort of "speed-dating" experience to evaluate them quickly.
This project was known as TodoMVC, and you can still find its content relevant for technologies like React, Vue, Svelte etc. It has become the goto place for checking whether a technology fits your purposes or not.
HTMX
If you’re in tune with web dev trends you’ve probably heard or read about HTMX. This is a small JS project, aiming to simplify the way we build web apps, relying more on server rendered markup and AJAX.
My interest in this technology stems from my "Back to Square One" approach, which is all about stepping off the frameworks and meta-frameworks roller coaster for a moment to take a fresh look at how we build things today—and explore ways to improve the experience for both us and our customers.
HTMX fits this approach pretty well, relying on much less JS code for heavy client magic, and more on web standards and the http protocol.
Opportunity knocks
Obviously the first place I went to check whether HTMX is something that one can rely on was the TodoMVC project, alas, there was no example written with this technology.
Hear that Opportunity knocking?
I can create this example and deepen my HTMX knowledge on the process, and hey - I might be able to contribute to the TodoMVC open-source project. This is a win-win-win situation. I like those.
First steps
Gladly the TodoMVC project has a great contribution docs and it also provides a starting template, along with a detailed application spec. I took the template and spec and started inspecting them to get a better understanding on how the app client is constructed (the markup) and what functionality needs to be supported.
This initial phase, as you will see later, will have serious implications on how the final HTMX app architecture will end up like.
HTMX app architecture
I believe the key takeaway from this article is that you can't approach HTMX app architecture the same way you would with reactive, client-heavy JavaScript frameworks. Doing so would mean missing out on HTMX’s unique advantages and could lead to frustration, making it feel difficult or unintuitive. Building with HTMX is a completely different discipline.
In HTMX most of the work is done on the server side, thus most of your architecture decisions are relevant for that tier, yet, you need to construct the client markup in a sensible way that will allow you to enjoy the different swapping strategies HTMX offers.
In the case of TodoMVC, I was, for better or worse, constrained by the template’s markup. However, I didn’t have much desire to change the template anyway, as I wanted to see how HTMX would perform within these limitations.
Server architecture
I will be using the C4 model in order to visualize the “server container” architecture.
The server container has 4 components in it:
Component | Description |
---|---|
TodosRouter | Defines the routes the client uses to interact with the app |
TodosAPI | The "Model" - Holds and manipulates the todos data, handling all CRUD ops |
Templating engine | The "View" - Renders HTML from the data |
TodosController | The "Controller" - Handles requests, using TodosAPI to compose HTML |
Yes, you could do the entire thing in a single file, but since the application here is very conservative it allowed me to think deeper on how I would see an HTMX driven application being code-designed.
One of the aspects of HTMX that really appealed to me is that the client directly receives content it can immediately parse and display, rather than receiving a JSON data structure that then has to be converted into markup.
This means there must be a component responsible for "translating" the data into markup. That tier would be the templating engine. This approach allows us, perhaps in the future, to change how we “translate” data into markup without altering other parts of the system.
The component which orchestrates it all is the controller, and it works with the API to get and manipulate the data, and then takes the result and passes it through the templating engine component in order to send markup to the client.
The diagram below describes it better:
The tech stack
It’s a good point to present the technologies we’re going to use for this application. Obviously these are my choices, and you can choose whatever alternative you feel comfortable with -
First, I’m using a NodeJS env. It’s the most intuitive for me plus it keeps JS across all.
For the app server I’m using Fastify. The templating engine is EJS. The client would be plain ‘ol HTML, JS and CSS with the help of HTMX.
That’s it.
The page’s parts
It was tempting to divide the page into little components and treat each as a template, but this is exactly where I found myself needing to insist on taking a different approach.
It narrows down to HTMX ability to perform an AJAX call instead of making a “conventional” http GET request to render the app.
While an http GET will refresh the entire page, causing all the resources to be re-fetched, the AJAX call allows me to replace a certain portion of the doc without the need for refresh, but this requires some architectural mind-shift.
The diagram below represents the application’s parts -
Let’s go one by one and understand what each template is responsible for -
index.ejs
This template is what's returned when we request the page from the root url. It is responsible for loading the app’s CSS and JS required, and inside of it it embeds the todos.ejs.
Notice that the input for “What needs to be done?” is part of the index.ejs since there is no need for it to be refreshed from the server.
todos.ejs
This is the main application. It holds the Todos list and the footer at the bottom.
I wanted to bundle them together since this is the part which gets refreshed whenever we add, remove, toggle or even filter todos.
This template is also the one which has the hx-triggers to load the data when events are triggered from the server. Basically, this is the place which listens to what happens in the application and refreshes the application part accordingly.
todo-list.ejs
This is the todo list, a simple UL with a loop going over the given todos and creating an LI element for each.
In the original TodoMVC template, this section is also responsible for rendering the “toggle-all” button, found on the left side of the input. I wanted to keep this structure, though I’d probably not constructed it this way to begin with.
The “toggle-all” button is using hx-patch
to make a PATCH request to the /todos/toggle
endpoint in order to toggle all the todos.
footer.ejs
This template is responsible for the todo count, filtering, and clearing all completed todos. It only appears when there are todos in the system and updates whenever a todo is added, deleted, or marked as completed or active.
It uses anchor tags for the filters, navigating to a URL that includes the filter. However, we’re using HTMX boosting here to ensure that we don’t refresh the entire page, but only fetch what is needed.
single-todo.ejs
This one renders a single todo, but it is not that simple.
You can toggle the single todo by clicking on the checkbox, and you can also edit the todo’s label by double-clicking it.
I’m using hx-patch
to make a PATCH request to the /todos/toggle/:id
endpoint (now with the corresponding todo’s id), and I’m using hx-patch also to make a PATCH request to the /todos/edit/:id
endpoint to edit the label.
Rendering & boosting
HTMX, if you allow it, makes you think of an application rendering in 2 ways - either you refresh the entire doc upon state change or you swap different sections in the page according to a server state change.
This means that you need to design the document content in such a way that you can support both methods.
Our application has a hx-boost="true"
attribute at its body level. This means that it will be applied to all nested in it, since this attribute is inherited.
In TodosMVC case it can be shown when requesting the application for the first time in comparison to fetching just the internal todos markup (that is the todos list and footer)
Accessing the app for the first time
To get a better understanding, here is a sequence diagram of fetching the application for the first time:
When this is the first time the application gets rendered, the GET request is not boosted, meaning that we want to get the entire markup for the main document, and not just parts of it.
HTMX knows how to append a hx-boosted header on requests which are boosted and it is something we can query later on the server and know which action should be performed. As can seen in the example below:
fastify.get('/', async (request, reply) => {
let isHxBoosted = request.headers['hx-boosted'] === 'true';
const {filter} = request.query;
const markup = isHxBoosted
? await this.todoController.renderTodos(filter)
: await this.todoController.renderIndexPage(filter);
return reply.type('text/html').send(markup);
});
In the example above we render the entire index page and return it back to the browser.
Filtering the todos
Let’s look at what happens when we filter the todos by clicking the “completed” filter button on the Footer:
We perform a GET request with the filter set to “completed”. The request is boosted and the server knows not to do a completed render to the page, but just the todos part. It renders the todos.ejs template with the filtered data and returns this markup to the client, which knows to swap it in the right place.
Editing a single todo
What happens when we edit a single todo item?
In the example above we send a PATCH request (yes, we try to stick to ReST protocols the best we can) with the new label and the todo item’s id, from there we update the todo and render a single todo item, by rendering the single-todo.ejs template. Once we have its markup we return it to the client which knows to swap it in the closest LI element to the element which triggered this request.
Adding a new todo
And when we’re adding a new todo? Here we’re using the HTMX events -
In the example above we use the great power of HTMX events.
The first action is easy to understand, we send a POST request for the new label to be added to the todos list.
Once this task is completed, we do not return any markup, but rather return a response that has a hx-trigger header with an event called “todoCreated”.
This event can then be listened upon on the client, and when it receives it can perform actions, like refreshing the todos.
Registering to this event is done on the todos.ejs template where, as you can see, we also listen to other events
<section
class="todos"
hx-get="/todos?filter=<%= filter %>"
hx-trigger="todoCreated from:body, todoDeleted from:body, allToggled from:body, singleToggled from:body"
hx-swap="outerHTML"
>
In most of the cases, there is a need to fetch the entire inner todos markup, that is the todos list and the footer. The most optimized way is to fetch it all in a single request instead of separating it to several requests, each for a different part of the app. See more about it in the “Thoughts” parts below.
Toggling and cleaning
Both are done with the same techniques shown above. You can check out the code on GitHub to see how it is done,
Filtering
The footer holds a filtering section where you can choose between “Active”, “Completed” and “All” filters.
The way it is done is by navigating to a URL which holds the filter type as a request param. The A href looks like this:
<li>
<a class="selected" href="?filter=active">Active</a>
</li>
But as you guessed it we do not want a full page reload when a filter is selected, and again the hx-boost
comes to our help and the request for the “index” page is boosted and our server knows how to handle this and returns only the relevant markup.
In the footer.ejs
we set that the response target will be the .todos selector and we’re set to go:
<ul class="filters" hx-target=".todos">
And that’s it. We have a fully working TodoMVC app built with HTMX :)
Thoughts
Client side logic
At certain places in the client code I found myself in need to add JS code, using “real” vanilla JS in one place and HTMX syntax in another.
One example is clearing the input for new todos when a submit for a new todo was made. It looks like this:
(function (window) {
'use strict';
// Clear the todo label input after creating a new todo
window.document.body.addEventListener('todoCreated', () => {
const todoLabelInputElement = window.document.querySelector('.new-todo');
todoLabelInputElement.value = '';
});
})(window);
Now, I could have done this by returning a blank todo from the server upon creating a new todo, but I thought it was an overkill (WDYT?).
There is no rule to avoid using JS on the client, and you should not avoid that when it makes sense, but I think that the approach should be “JS should be used to enrich/hydrate the markup we receive from the server and not for creating the DOM in the first place”.
I’m cool with the decision of performing this cleanup in JS on the client side.
Switching styles upon editing
When you double click a single todo item, it changes style class to “editing” and also when you blur the input it switches back. This transition is done by listening for those events using the hx-on syntax and then executing the event handler with inline JS. Yikes… I know.
<label hx-on:dblclick="this.closest('li').classList.replace('<%= todo.status %>', 'editing');this.closest('li').querySelector('.edit').focus();"><%= todo.label %></label>
And on the input, check the hx-on:blur
:
<input
class="edit"
name="label"
value="<%= todo.label %>"
hx-on:blur="this.closest('li').classList.replace('editing', '<%= todo.status %>');"
hx-patch="/todos/edit/<%= todo.id %>"
hx-target="closest li"
hx-swap="outerHTML"
/>
</li>
Not very elegant, but I feel that as long as it is kept at a reasonable scope,it’s fine. What bothers me a bit is that we’re keeping a state (the todo.status) on the client, and one of the things I like about HTMX is the fact that it forces encourages you to keep the state in a single place - the server.
Having said that, calling the server each time to change the style is a bit of an overkill. If you have suggestions on how this can be more elegant while still performant, do share in the comments below.
Requesting the entire todos markup each time?
For quite a few scenarios I’m requesting the server to render the todos markup, which consists of the todos list and footer, over and over again.
Even with the HTMX boosting it feels a bit too much… but is it?
Yes, reactive applications, which do some heavy lifting on the client, know to listen to changes and render the specific elements accordingly, and when I look at the DOM changes I kinda wish I could do the same with HTMX.
Well, in theory, I could.
I could separate the changes into different http requests, but it felt like going too “religious” over this. Yes, the application core renders again and we have a request going out of the browser again, but receiving a markup which represents the single source of state found on the server kinda makes me peaceful about it. It is not like we make a request to the server for an initial state and from there on we’re juggling between 2 potential states, one on the client and one on the server, hoping that they will be synced in the end.
Don’t know… still thinking about it.
Going offline
I think that this is the biggest concern here.
If the application is offline, nothing can render as expected on the client. It is not like we fetched the entire code needed for the client and from there on we can still perform actions on the client and buffer them in case of offline network. Here it is a bit more complicated.
There are ways to mitigate it using cache techniques or service-workers, but I think that when you choose HTMX you need to understand that your client is highly dependent on network connection.
Conclusions
This was a really interesting challenge. I learned quite a lot on how HTMX works and what it means to create an architecture for an application using this technology.
I find HTMX very appealing but I understand from what I read out there that there are still places where HTMX fails. I’m not entirely convinced yet, since I think that most of these failures derive from trying to approach HTMX applications in the same way you would with Reactive technology. It’s not.
This implementation is very naive, but it is a good start to understand the potential of HTMX and also where it falls short. I think that the integration with WebComponents can be an interesting evolution to this project.
I really like the idea that HTMX strips down much of the complexity of building a web app these days.
As said the code can found here: https://github.com/mbarzeev/todomvc/tree/htmx/examples/htmx
You can also check out the PR I’ve made on the original TodoMVC repo, and I’d really appreciate you voting for it so that the maintainers will review and consider including it (even when the repo is no longer actively maintained).
Hope this helps and inspires you,
Take care
Top comments (0)