HTMX with Java Spring Boot and Thymeleaf
I like to think of myself as a back end developer. However, having used so many different frameworks over the years to consume JSON APIs to create web front ends, if I had to write a CV now I'd probably feel obliged to use the ignoble term "full stack developer". But then I'd probably blow it by feeling the need to explain that doesn't mean I just splatter JavaScript everywhere 😏.
Having used jQuery, Backbone, AngularJS, React and React plus Typescript, I have a bit of a love / hate relationship with JavaScript frameworks. Reasons include:
- Many of them (and especially the more "recent" ones) seem like overcomplicated time sinks for the "CRUD+" apps I tend to write
- But to be fair I typically only write a few apps a year in them so I never get to the "super familiarity stage" with these front end frameworks - But by far their biggest crime is the "bit rot" that sets in almost as soon as you finish an app. Coming back in a year and wanting to update an app? Good luck with that if you are not prepared to spend hours or days getting the build to work again and/or working through "dependencies upgrade hell". Compared to Java's legendary backwards compatibility (JDK8+ repackaging aside), it is just so much self inflicted pain
- But too be fair, I have no problem working through Spring Boot deprecations whenever I have to
- And to be even fairer, my jQuery apps (no real build stage) and the Backbone ones (a simple RequireJS based build stage) don't suffer from this.
So I was looking for an alternate approach to writing the fairly simple front ends I tend to need to write for my back end applications.
I'd heard about HTMX but written it off as it returns HTML to the client. How could such an API be re-used by different clients? Eventually I started thinking about just how often any JSON API I'd provided ended being consumed by a different front end business client without changes or additions. "Not that often" was the answer.
OK - so I decided to give HTMX a go.
The existing application
I already had a suitable test application - a Java Spring Boot "microservice" written a few years ago to use when trying out frameworks like Docker and Docker Compose and getting caught up in the whole "it must be scalable" thing. It's about 4K LoC - so a bit more fleshed out than the average "To-do list" demo and enough to hopefully cover some real life unhappy path scenarios.
Over it's life, the microservice has had front ends written with AngularJS, React and then React/Typescript. So it seemed like an ideal candidate for re-implementing with HTMX thus allowing me to write clickbait links along the lines of "React ditched for HTMX!!!".
So welcome to The Cloudy Book Club!
- The live, HTMX based app can be found at https://cloudybookclub.com/
- The older, React/Typescript based app can be found at https://spa.cloudybookclub.com/ - it should look very, very similar to the first link!
Because the parts of the application with the most client side interactivity are behind admin logon, here's a short screen cast of some of the functionality.
Screen recording of main application functionality (53 seconds / 19Mb)
Running Docker?
If you have Docker running, you could try running
docker compose up -d
in the root of the checked out project.
If you have no "port clashes" with anything else running locally on 80, 8888 or 8761, when you point a browser at http://localhost/ you will now be able to access a locally running instance of the application, seeded with some sample data and you will have been auto-logged on with full admin privileges.
So Java 21, the latest Spring Boot and HTMX were givens but that left decisions to be made about
- Which Java templating solution to use to create the HTML pages and snippets
- what tweaks were going to be needed to the Spring Boot configuration to have a good "front end" development experience and a performant web app
Both of these will be covered in the next part of this blog post and then we'll get back to how well HTMX worked out as a React replacement!
Why Thymeleaf?
Like most Java developers I'd come across Thymeleaf before. However, again like many Java developers I expect, my only use of it was for templating HTML emails. I'd skipped on from JSPs with JSTL (urgh - especially when used without discipline) to consuming JSON APIs on the front end. So not much previous real exposure to Thymeleaf - I'd be starting pretty much from scratch.
The choice for HTML templating in Java quickly boiled down to either Thymeleaf or JTE. Here's my take on the pros and cons for Thymeleaf.
- "natural templates" - see below for why I care about this
- a good match for HTMX with its support for "template fragments"
- a long lived project so very likely to be around this time next year
Cons
- it doesn't have any type safety and no support for IDE "intellisense" (which is a huge advantage for JTE)
- it has hideous stack traces (and that's from someone who has worked with Java stack traces for a quarter of a century!)
Above all else, it was the "natural template" support in Thymeleaf that swung it for me. What does this mean and why do I care?
If we go back to the bad old days of server side JSPs, they held a conglomeration of HTML, JSP tags and, if you were unlucky, a bunch of Java. This made the hand off between UX designer and the code developers pretty horrible - there was certainly no "round tripping" of assets between the UX team and the development team! It was one way traffic (to the development team only).
With Thymeleaf natural templates, you can open the template (an HTML file) in a browser and it will look exactly like it will when it is fully populated by server side data (without reams of live data of course). Go on, try it! Check out the project from Github (details at the end of this article) and then open any of the files in <project root>/books/src/main/resources/templates in your favourite browser and see how it looks. In fact, this is how I did most of the UX tweaks required when moving from the React based implementation.
Spring Boot and Thymeleaf Development
Having got used to all the functionality in the various AngularJS and React build tools I've used over the years, I wasn't keen to lose things like hot reload, static file finger printing for caching, minification and bundling. How well could I get Spring Boot and Thymeleaf to play together to give me development and production environments I could live with?
Turns out this was mostly all good news. With the checked in application.yml configuration file and spring-boot-devtools added to pom.xml, I get
- automatic file finger printing of static files including CSS, JS and images
<!DOCTYPE html>
<html lang="en-GB">
<head>
<title>Cloudy Book Club - Home</title>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<meta content="Aidan Whiteley" name="author">
<meta content="Cloudy Book Club" name="keywords">
<meta content="Books read recently (so I don't buy them again!)" name="description">
<meta name="htmx-config" content='{"selfRequestsOnly": true, "allowScriptTags": false, "allowEval": false, "includeIndicatorStyles": false }' >
<link href="/css/bootstrap.min-600874cbb0b828ca80136b5652c40076.css" rel="stylesheet">
<link href="/css/bootstrap-icons.min-90c6ea83dbdf2b22d1f5ea20b281fa7f.css" rel="stylesheet">
<link href="/css/swiper.min-1fd9ab176747f4c54cb99232939de368.css" rel="stylesheet">
<link href="/css/tom-select-f09c3e22360beccda7841a0e818cc0ee.css" rel="stylesheet">
<link href="/css/toastify.min-d93909335e384d8afd95bfcff0cb6e97.css" rel="stylesheet">
<link href="/css/simple-data-tables-6bbe32740439b942481324a973894881.css" rel="stylesheet">
<link href="/css/books-app-ba82e124d2fffd54d58184a682b1163b.css" rel="stylesheet">
<script src="/js/htmx-2.0.4.min-19a573773be4ca22570ca2f8543120c5.js" defer></script>
<script src="/js/htmx-response-targets-2.0.3.min-b6f8377a994183ef35de6472ed7b3f86.js" defer></script>
<script src="/js/bootstrap.bundle.min-bd31cd6c0de0972f2e5d260a09ba121f.js" defer></script>
<script src="/js/toastify-js.min-2fe90ab79959d92871ac3547ba420565.js" defer></script>
<script src="/js/swiper-bundle.min-0b452598533f4d711a65cbb7bf5fb050.js" defer></script>
<script src="/js/tom-select.base.min-fe98e1ec822305179fce95f4c0b3d19a.js" defer></script>
<script src="/js/buttons.min-fb70d302f9ee68f83cea036816077ff8.js" async defer></script>
<script src="/js/books-e2cc524622a0b1d65ac09cbc5af1d218.js" defer></script>
<script src="/js/stats-97c742b5795f27c6bfc8724cb08b8cc7.js" data-website-id="1e82a5f1-8ae6-4441-a684-3714a1d9a18b"
data-do-not-track="true"
data-domains="cloudybookclub.com"
data-host-url="//stats.aidanwhiteley.com" defer></script>
</head>
<body hx-boost="false" hx-ext="response-targets" hx-history="false">- changes to CSS and JS files are (pretty much) immediately applied on browser reload
- automatic disabling on the Thymeleaf caches so that template changes are immediately visible on browser reload
- hot reload of the running application on any Java code changes. This is OK but not perfect in that it typically takes a few seconds to "notice" the change and then a few seconds to reload the app. So a "cycle time" of less than 10 seconds which isn't too bad and way less than JRebel assume!
So not bad (and I haven't tried tweaking it) but still, if I'm honest, a little bit slower than the Vite based build of the previous React/Typescript front end implementation. But it's a price I'm pretty happy to pay to avoid having a separate front end build process (which can't, therefore, go stale 😏).
What I don't get is automatic minification and bundling of the CSS and JS files. However, I think that is pretty much a non-issue as
- all 3rd party files are "vendored" into the application and are already minified versions
- the live application uses HTTP2 and HTTP3 (all the way back to the source server) so the 40 or so small files used in the home page load quickly and mostly in parallel (if we don't count the files from Google Books API). A recent Lighthouse Report seems to agree.
React to HTMX Challenges
Re-implementing the front end of the application to use HTMX rather than React had some challenges.
Client side state?
Here's an example of the issue. If you watch the screen cast, the "Add a book review" page offers the user a set of book reviews / book covers from the Google Books API that match the user entered book title and author name. The user can scroll through these, with the "Add a book review" form partially completed, to pick the best match.
In the React based front end, the data from the Google API call is passed down to the page as JSON and then the Previous and Next buttons iterates that data - all client side. I guess I could have done something similar with plain old JavaScript but that just didn't feel very HTMX ish!
The server side Books application, from day one, went to some considerable effort to be completely free of HTTP session state. So the Google Books API data can't be stashed there. In fact, the list of matching books obviously isn't specific to the user so it was put into a Mongo collection with a short TTL index. So as the user clicks Previous and Next, an HTMX hx-get is made and an hx-swap of "outerHTML" takes place.
<button aria-label="Previous" class="btn btn-outline-primary previous-button"
hx-swap="outerHTML"
hx-target="#googleBookCandidates" hx-target-error="#detail"
th:disabled="${googleBookSearchResult.hasPrevious == false}"
th:hx-get="@{/googlebooks(index=${index - 1},title=${booktitle},author=${author})}"
type="button" data-umami-event="Scrolled to previous Google book review">«
Previous
</button>So, in summary, some parts of your application may have to be re-designed / re-coded if migrating an existing application.
Thymeleaf and HTMX attributes
The above code snippet is also a good example of the need to mix Thymeleaf and HTMX attributes - I called it "layered tagging". The Thymeleaf values are set server side and the HTMX attributes run on the client side.
Figuring out how to make both play together nicely was sometimes problematic. For example, I failed to be able to get Thymeleaf to write unescaped JSON into the HTMX hx-headers attribute to add an XSRF (cross site request forgery) token to HTMX Ajax requests. I had to fall back to using a JavaScript event listener for the htmx:configRequest event to intercept HTMX Ajax requests to add the XSRF token (code implementation).
HTMX and 3rd party JavaScript components
The application uses a few 3rd party JavaScript components - specifically
- Swiper for the book covers carousel
- toastify-js for the "toast" notification functionality
- tomselect for the typeahead dropdown functionality
- DataTables for the client side table functionality used in the user admin page
These components need to be initialised and, more importantly, re-initialised after HTMX has been has been used to pull in updated HTML. The HTMX documentation has some guidance for this. My implementation typically sets an HTTP header on specific HTTP request responses (when issued by HTMX) and then has a JavaScript event listener listening for the HTMX generated event e.g.
if (hxRequest) {
response.addHeader(HX_TRIGGER_AFTER_SWAP, "initSwiper");
}document.addEventListener("initSwiper", initialiseSwiper);For the fuller details, see the books.js file.
As an aside, I wasn't planning on using hx-boost but (as per the HTMX docs)it isn't possible to use hx-boost when you have references to the 3rd party JavaScript components held in mutable JavaScript "global" variables.
HTMX and security?
There's been some murmurings about whether HTMX is secure enough for the average webapp - particularly with regard to Content Security Policies.
Although my app is basically a play thing, it is meant to exhibit good, safe practises. Therefore, I wanted it to have a reasonably robust CSP (while CSPs remain the least worst security back stop against coding errors). Therefore, the following HTMX settings were applied
<meta name="htmx-config" content='{"selfRequestsOnly": true, "allowScriptTags": false, "allowEval": false, "includeIndicatorStyles": false }' >The app also follows most of the best practises detailed in the "HTMX Security" and "HTMX - Web Security Basics" pages - noting for the 2nd reference I'm much happier using something like JSoup to sanitise user input (where required in code) than using regular expressions!
The CSP I ended up with is:
connect-src 'self' api.github.com stats.aidanwhiteley.com; script-src 'self'; font-src 'self' data: ; img-src 'self' books.google.com data: ; script-src-elem 'self'; style-src-attr 'self' 'unsafe-inline'; style-src-elem 'self' 'sha256-Jb11pePfz0oLKVzEK6WLuM/bMRvHS2bnnMEROG6KDR8='; object-src 'none'; base-uri 'self';
The key impacts of the HTMX configuration and the CSP to highlight are:
- allowEval is set to false. This means that the hx-on HTMX attribute will not work. This would have been easy to work round, if needed, with app specific JS and HTMX events.
- Whether using hx-on or not, the decision was to break with the "locality of behaviour" pattern for application JavaScript and put it all in an external file. This meant there was no "cop out" in the CSP of allowing "unsafe-inline" for script-src!
- Observant readers will have noticed that "unsafe-inline" is allowed for the style-src-attr. A previous HTMX GitHub issue pointed the way to start with but the number of 3rd party components dynamically updating style attributes in the DOM meant I gave up on this one!
So, if there are coding errors, hopefully data can't be exfiltrated from the site but it is at risk of defacement attacks. Possibly.
HTMX or HTML?
As an example, HTML provides the "form post" tags and HTMX provides the hx-post attribute. Which one should you use to submit data to the server and why?
My take on this is that you should be driven by the user experience requirements but prefer the plain HTML option if possible. Here's a similar view.
In the application, the "Create Book Review" form seems like a natural candidate for an HTML form post. However, I had a self-inflicted "requirement" to show an action confirmation "toast" notification after the form submit. Given that HTML form posts are typically backed by a post-redirect-get, the necessary information to show the "toast" notification would be lost with an HTML form post (if not using HTTP session state or something more complex).
So, in this case, hx-post was much preferred!
<form th:if="${user != null and (highestRole == 'EDITOR' or highestRole == 'ADMIN')}"
th:action="@{/dummy}" class="row g-3" id="createreviewform" method="post" novalidate
th:hx-post="@{${actionUrl}}" hx-target="#detail" th:object="${bookForm}">
...
</form>HTMX or React - the conclusions
The following "pros" and cons" are applicable to me and the applications I typically develop. Therefore, they ignore key issues like availability of skilled developers.
- Great for applications with lots of front end state (i.e. state that is not directly driven by back end state)
- Masses of available, existing React components or components with React wrappers
- Masses of best practice information (but choose from them wisely)
React Cons
- Constant churn on React libraries and frameworks leading to both major version upgrade work (e.g. React Router) or forced change of toolsets (e.g. Create React App)
- The more recent positioning by the React team that React development should be done with a framework (e.g. Next) and preferably a full stack one at that
- For me, a lot of "foot guns" with the Hooks based React. The correct usage patterns are well known but, if like me, you are only only an intermittent React user, your feet are well at risk!
- Simple front end development with a fairly shallow learning curve
- A good development experience leading to performant web apps
- just one build process for the whole of the app and I'm confident it will 'just work" when I need to change the app in a year's time
- The fun of playing with new tech!
HTMX Cons
- It really isn't the right tool if your requirements drive the need for lots of complex front end state
So, yes, my next app is very likely to be a Java Spring Boot, Thymeleaf and HTMX one!
Github Repository: https://github.com/aidanwhiteley/books
Explaining my biases
To help explain my biases that I have, no doubt, exposed in the above article, here's a little bit about my tech background. While I've always called myself a back end developer, it's not strictly true.
On the desktop/backend, I've mainly used Rexx, JCL, Cobol, Visual Basic 3 +, Visual C++ 4 +, Perl, Java, Scala and more Java
On the front end, it's mainly been ISPF, JSPs, plain old JavaScript, JQuery, Backbone, AngularJS, React and React/Typescript
Oh, and I was deeply scarred by having to use server side JavaScript in the early 2000s with an eCommerce product called Broadvision! Still better than AEM, eh?