HTMX with a fake backend - hypermedia-server
HTMX is an awesome tool to quickly add some dynamic behaviour to the frontend of applications, without alot of the machinery associated with feature rich frameworks like react, svelte or vue.
When starting projects, I’m often looking to translate a rough design or flow into interactive pages. Figma can be great first step to move towards a clickable prototype, but what’s the next? HTMX fits in really nicely to this stage, where I’d like to shift over into real markup and begin fleshing out some of the high-level model objects. The question I ask myself is often, how do I make an experiment real without alot of complexity? When is it time to reach for django/rails? Similarly, do I really need to start projects with React? Svelte? Next.js?
I think the usual decision for people is whatever you’re comfortable with, in my case Django and Lit or React. But lately I’ve been exploring this in a bit more detail. What if I just… didn’t introduce these tools at this stage? How far can I get without this stuff? What’s missing or time-consuming to get going?
In this post I’m going to introduce an extension of the API faking library json-server
that adds hypermedia responses. Instead of a RESTful api returning JSON, we render that same data through through simplistic templates, returning html blocks. When working with HTMX and markup, I find this has been a great tool to explore ideas quickly.
To pull it down:
git clone [https://github.com/zachgoldstein/hypermedia-server.git](https://github.com/zachgoldstein/hypermedia-server.git)`
Run it locally via npx
npx babel-node -- src/cli/bin ./examples/boops/db.json --hypermedia --template ./examples/boops/templates.json
Let’s serve our little example page quickly:
cd hypermedia-server/examples/boops
python3 -m http.server
Now if we go to http://localhost:8000/page.html
, we have a running CRUD app with no backend!

Let’s dig into this toy example in a bit more detail. Here’s what it’s doing with HTMX and a fake backend library:
- Click the boop button and a
POST
request to/boops
will create a new boop model object. - The
POST
request returns an html block for the newly created boop, which is shown as some confirmation text under our boop button. - A
<ul>
element shows all our boop model objects by issuing aGET
request to the plural/boops
endpoint. It’s triggered after our initial page load and when clicking the boop button. - The
/boops
endpoint returns an html block with A new<ul>
element, htmx is swapping our<ul>
object’s contents with the newly returned html block representing all boops. - Each
<li>
element representing a boop has a button on it to delete, update or get that boop model object. - Clicking the
DELETE
button on a boop issues aDELETE
request, deleting the boop and then swapping in the empty html block that is getting returned. - Clicking the
UPDATE
button on a boop issues aPUT
request with a new timestamp, updating the boop and then swapping in the returned html block with the updated boop. - Clicking the
GET
button on a boop issues aGET
request for that specific boop, returning it’s html representation and swapping in the returned html block with the updated boop.
Our page’s markup looks like this:
<body>
<button class="boop-btn"
hx-post="/boops"
hx-trigger="click"
hx-target=".new-boop">
Click to BOOP</button>
<div class="new-boop"></div>
<div class="boops"
hx-get="/boops"
hx-trigger="load, click from:.boop-btn"
hx-target=".boop-list"
hx-swap="outerHTML">
<h3>Boops:</h3>
<ul class="boop-list"></ul>
</div>
</body>
Full page at https://github.com/zachgoldstein/hypermedia-server/blob/master/examples/boops/page.html
Our hypermedia returning fake API will respond to a plural GET
request with an html block of all the boops in the db.json
file we provide, which acts as a very simple DB via lowdb. In that html block, each boop object has buttons to GET
, UPDATE
and DELETE
the resource.
The get-all-boops.ejs
template, returning a list of all boops:
<ul class="boop-list">
<% top.forEach(function(boop) { %>
<li id="boop-<%= boop.id %>">
<p><strong>Boop: <%= boop.id %></strong> time updated: <%= boop.time %></p>
<button
hx-delete="/boops/<%= boop.id %>"
hx-target="#boop-<%= boop.id %>"
hx-swap="outerHTML">
Delete
</button>
<button
hx-put="/boops/<%= boop.id %>"
hx-vals='js:{time: Date.now()}'
hx-target="#boop-<%= boop.id %>">
Update
</button>
<button
hx-target="#boop-<%= boop.id %>"
hx-get="/boops/<%= boop.id %>">
Get
</button>
</li>
<% }); %>
</ul>
And for a single boop model object we have get-boop.ejs
:
<li id="boop-<%= top.id %>">
<p><strong>Boop: <%= top.id %></strong> time updated: <%= top.time %></p>
<button
hx-delete="/boops/<%= top.id %>"
hx-target="#boop-<%= top.id %>"
hx-swap="outerHTML">
Delete
</button>
<button
hx-put="/boops/<%= top.id %>"
hx-vals='js:{time: Date.now()}'
hx-target="#boop-<%= top.id %>">
Update
</button>
<button
hx-target="#boop-<%= top.id %>"
hx-get="/boops/<%= top.id %>">
Get
</button>
</li>
For these templates to work, we provide a mapping file, templates.json
that looks like this:
{
"boops":{
"POST":"post-boop.ejs",
"GET":"get-boop.ejs",
"GET-ALL":"get-all-boops.ejs",
"PUT":"get-boop.ejs",
"DELETE":"delete-boop.ejs"
}
}
You can see both the PUT
and GET
endpoints are using the same template for a single boop object, since they’re both meant to return an html representation of a single boop object.
And then for each of the other templates:
post-boop.ejs
: Returning a simple status message that the boop was created
<div>Created a boop! ID:<%= top.id %></div>
delete-boop.ejs
returns nothing, so swapping the results of DELETE will remove an element.
A slight gotcha with htmx, in order for this to work you need to tell htmx to add the location of your locally-running hypermedia faking api to the request path:
document.body.addEventListener('htmx:configRequest', function (evt) {
evt.detail.path = "http://localhost:3000" + evt.detail.path
});
And voila! A dead simple CRUD application with htmx and a faked hypermedia API.
Caveats galore! This is not a real application
- The data is pretty ephemeral, sitting in a simple json file.
- The target usecase here is really CRUD applications, anything requiring more dynamic behaviour might be more tricky.
- You can’t really build non-trivial backend business logic.
- There isn’t really much “best practise” here, it’s more of an approach to quickly sketch out ideas.
See https://htmx.org/essays/when-to-use-hypermedia/ for a much more detailed take on when/when not to use hypermedia and htmx.
I think the best way to use something like this is an early way to get dynamic prototypes in front of people. You can throw this online with something like ngrok and then use it for an early research session. Beyond coached demos it’s probably time to pull in the longer-term tools.
My belief is that in using these incredibly pared down tools, you remove alot of the effort when experimenting with web applications. You can focus on learning about whether or not your main flows and data models make sense with minimal effort before tackling anything really meaty.
Lots of other stuff to explore in the future! A few ideas:
- Write up how to use
ngrok
andpython3 -m http.server
to get something public that would actually be interactive for somebody else. Hint: Use aconfig.yml
file with two tunnels, then change the page’shtmx:configRequest
listener to point at the endpoint ngrok give you for the faked api. - How would we reuse the templates as we shift to a full stack framework like Django?
- How do we evolve the frontend markup to longer-term solutions? How would a full featured framework like react get pulled in?
- If we wanted to make individual buckets of markup & functionality more maintainable, can we move incrementally into web-components?
- What would this look like if we replaced this faked API with the API wrapper of an excel-sheet-esque system like airtable?