abstract illustration with code elements

How to Simplify Web Development with htmx

In adjoe’s WAVE team, we continuously run A/B tests to measure the impact of our changes, evaluate the effectiveness of our bidding algorithm, or compare different user interfaces. 

Managing these tests previously involved manually setting up A/B tests through code adjustments and executing SQL queries. This process was prone to human error and quite cumbersome. It took a developer in our team up to a week to implement and launch one A/B test. 

To streamline this, we decided to develop a dashboard to create, manage, and monitor these experiments. Our primary requirement was to have a frontend interface intended exclusively for our internal teams, without the need for high levels of interactivity or performance. We aimed to build it as efficiently as possible with our available resources.

Which Framework Did We Choose?

React is the most popular framework for building frontends; we initially considered it due to its vast ecosystem of component libraries and tools. However, we wondered if an SPA framework like React was necessary. 

We also considered alternatives like jQuery or Vanilla JS. However, we ultimately chose htmx for its simplicity, maintainability, and excellent compatibility with Go. This choice allowed us to reduce development time. 

Setting up htmx is also straightforward. You only need to add a script tag to include the dependency – this is a sharp contrast to the complicated build systems common in other frontend technologies. The ability to use a common language across both the frontend and backend was a huge advantage and strongly influenced our decision to choose htmx. 

This led us to discover its advantages, which I will explore further in this article.

How Is Building with htmx Different?

Let’s first review the history of web technologies to better understand the htmx approach to web development.

Multi-Page Applications (MPAs)

Websites were initially primarily multi-page applications (MPAs) that interacted with servers in two main ways. 

The anchor tag first instructed the browser to fetch a new page via an HTTP GET request and replace the current page with the response. HTML forms then permitted data submission to the server using an HTTP POST request, which often redirected users to a new page afterward. 

These interactions were simple and straightforward; the server responded directly with the HTML to be rendered, but also required the entire page to be refreshed with each interaction. Native HTML was also limited to only sending HTTP GET and POST requests. This limitation led to the adoption of JavaScript to enhance user experiences and interactivity.

Single-Page Applications (SPAs)

When it comes to single-page applications (SPAs), server interactions have changed significantly. In SPAs, clicking a button might trigger an HTTP request of any method, and servers typically respond with a JSON object. 

The JavaScript running in the browser uses this JSON to update the state of a domain model maintained internally. This model then refreshes the relevant parts of the webpage, allowing for a more interactive user experience.

However, this method requires the client to understand the exact structure of the JSON response and its meaning. The client must recognize which further actions are valid and available, given the data provided. It must know how to render this data to the UI.

Additionally, the client needs to be updated whenever there are changes to the API. While the SPA approach enables a much more interactive and richer user experience, it comes at the cost of increased complexity.

Hypermedia-Driven Applications (HDAs)

Hypermedia-driven applications built with htmx, on the other hand, extend the capabilities of HTML while preserving the simplicity of the MPA approach. 

htmx generalizes the capabilities of standard HTML, enabling any HTML element to initiate HTTP requests of any method through simple attributes. The requests can also be triggered in response to a variety of user interactions beyond traditional mouse clicks and submit events. Critically, htmx facilitates updating only specific parts of a webpage – targeting and replacing subsets of the HTML DOM.

Since rendering occurs on the server, the client does not require additional logic or knowledge about which states are valid for which entities. The server directly returns the valid state along with interactions that are available in that state of the system, and the browser renders it. 

With these features, HDAs maintain the straightforward architecture of traditional web technologies while facilitating the creation of interactive user experiences like those found in modern SPAs. 

graphic of htmx and how it has worked since 2004
Source: htmx.org

How Does the Dashboard Work?

The core feature of our dashboard was an experiment creation wizard. This wizard enabled users to create and modify various attributes of an experiment using various forms. A typical recurring pattern that we used can be seen below.

<form hx-put={ fmt.Sprintf("/experiments/%s/information", experiment.ID) } hx-target-error="#notifications" hx-swap="innerHTML">
   // Input fields
<div id="notifications"></div>
screenshot of what htmx dashboard looks like in the browser

Here, we utilized the hx-put attribute to instruct htmx to send an HTTP PUT request to the specified URL. This carried form data within the request body. 

We also used the hx-target and hx-target-error attributes to manage the server’s response. If the server returns a success status, the response HTML replaces the content specified in hx-target. 

For error responses, such as a 500 status code, the HTML updates the area defined by hx-target-error, which is convenient for displaying validation errors to the user. The hx-swap attribute defined how the response would replace the target. Options included appending to the target or replacing its inner HTML, among other modes.

In the example above, we did not use an hx-target because, upon successful form submission, we redirected the user to the next page of the form using the “HX-Redirect” header in the response.

We also found htmx to be effective for building the group creation page. This is a key part of our dashboard that manages the distribution of different treatments among user groups. This page featured an interactive table that enabled users to easily add, update, or delete rows.

templ GroupsTable(experiment ui.Experiment) {
      <label>Experiment Groups</label>
      <ul>Column headers</ul>
      <ul id="groups">
         for _, group := range experiment.Groups {
            @GroupRow(group, editable)
         <button hx-post={ fmt.Sprintf("/experiments/%s/groups", experiment.ID) } hx-target="#groups" hx-swap="beforeend">Add</button>

templ GroupRow(group ui.Group, editable bool) {
      // Editable Columns
         <button hx-delete={ fmt.Sprintf("/experiments/%s/groups/%s", group.ExperimentID, group.ID) } hx-target="closest li" hx-swap="outerHTML">Delete</button>
screenshot of what experiment update interface looks like in browser

The GroupsTable component, as seen here, is part of a form that uses htmx to enhance functionality, similar to the pattern described earlier. The “Add” button within the table initiates a POST request, which returns the HTML for a new group row. 

This new row is then appended to the end of the list, thanks to the hx-swap=”beforeend” attribute. This instructs htmx to add the response HTML at the table’s end.

Each row in the table includes a “Delete” button that, when activated, does not return any HTML. Instead, the htmx attribute hx-target specifies that the closest li element — the row itself — should be removed. 

This functionality demonstrates htmx’s capability to target HTML elements dynamically using not just CSS selectors like the id tag, but also selectors such as closest, next, and previous. This setup showcases the simplicity and effectiveness of creating an interactive table with htmx, eliminating the need for additional JavaScript. 

Using just a few patterns like these, we successfully implemented the entire experiment management dashboard without any JavaScript. We also successfully deployed it thanks to htmx’s simplicity. It proved to be a perfect fit for our requirements. 

Choosing htmx allowed us to deliver a more convenient and reliable way to manage our experiments. This resulted in tighter iteration loops and quicker product improvements – whether for the UI that users interact with while watching in-app ads or the algorithms used for bidding on ad inventory.

When Should You Use htmx?

htmx is well-suited for scenarios where:

  • your application requires minimal to moderate interactivity
  • offline functionality is not a requirement
  • the core value of your application is derived from backend logic and server-side validation
  • the UI state does not require frequent updates

Its ability to deliver server-driven interactivity with minimal client-side code makes it a versatile and effective solution for building rich user experiences.

Of course, you can use htmx for some parts of your application while also integrating it with more interactive frameworks like React for other parts that demand greater interactivity. Choosing the right tool for the job is essential, and htmx offers a great option for web development where speed and simplicity are priorities.

Programmatic Supply

BI Analyst (f/m/d)

  • Full-time,
  • Hamburg

Data Analyst (f/m/d)

  • Full-time,
  • Hamburg

Senior Data Engineer (f/m/d)

  • Full-time,
  • Hamburg

We’re programmed to succeed

See vacancies