Motivation
Most apps I touched in the wild follow the same two tiered approach. A backend delivering JSON (some may call this REST) and a frontend framework, consuming JSON from the backend converting it to the HTML displayed to the user. Worst case, one team manages multiple frontends or backends at once.
For most business apps this setup is already more complex than necessary. With this post I want to propose a simpler approach to the typical business app, centered around a Spring Boot backend. No worries I won’t torture you with JSF or Primefaces.
The goal is to reduce the accidental complexity in the implemented system.
With Stackoverflow and Shopify there are examples in the wild of hugely successful enterprises with monolithic core applications. Of course at their scale this also has downsides and both are currently investigating how to make their codebase more modular. But if these two managed to grow to this scale with a monolith it seems not unlikely that this architecture, with adequate discipline, is sufficient for a lot of apps.
The two key elements I will use to make developing this application a pleasant and simple experience are:
Apache Freemarker as templating engine
HTMX in order to make the frontend more responsive
I will take you on the journey which lead to this application. I will describe the compromises and decisions made. If you are in a hurry or impatient you can simply check out the accompanying Git Repo and follow the README to build and deploy the application to AWS Lambda.
Templating engines
First things first. We want to render HTML on the server painlessly, so we need a so called templating engine which lets us define a certain layout with placeholders and provides a mechanism to fill these placeholders with real values.
I investigated different templating engines Mustache, Handlebars.java, Thymeleaf and Apache Freemarker.
The requirements where:
Simplicity: I know this is highly subjective. The whole reason for the app and the blogpost is to avoid accidental complexity wherever possible and reserve the complex solutions for the complex problems.
Component reusability: In most apps there will be at least a few (possibly many) recurring elements to be used at various locations throughout the app. Think about buttons. As we want to avoid duplication, we need a way to define these components at one place and use them wherever we need.
Mustache
I started with Mustache because it promises simplicity. There is a Spring Boot starter, which means it is easy to include. Code reuse in Mustache is implemented using so-called partials. Partials in Mustache sadly cannot receive parameters (e.g. Button text). Therefor Mustache cannot be used to create atomic reusable components.
Handlebars.java
The next natural stop is Handlebars.java.
It extends the Mustache syntax and offers a way to parametrise partials.
This can be used to build components.
With syntax like {{> content}}
inside the component you can create a named slot where other things could be inserted.
A full example would look like this:
site.hbs
will include the layout.hbs
file and replace certain parameters within the layout partial.
layout.hbs
1<div>
2 <div class="nav">
3 {{> nav}}
4 </div>
5 <div class="content">
6 {{> content}}
7 </div>
8 {{#> footer }}
9 Footer default
10 {{/footer}}
11</div>
site.hbs
1{{#> layout}}
2 {{#*inline "nav"}}
3 My Nav
4 {{/inline}}
5 {{#*inline "content"}}
6 My Content
7 {{/inline}}
8{{/layout}}
Together it would render to:
1<div> 2 <div class="nav"> 3 My Nav 4 </div> 5 <div class="content"> 6 My Content 7 </div> 8 Footer default 9</div>
To play around with it yourself you can have a look at the playground
The syntax with curly braces, hashtags and greater than does not feel super intuitive to me, so I searched further. But if you feel different, be my guest and use what suits you.
Thymeleaf
After this I had a look at the "Industry Standard" Thymeleaf. The mixture of templating directives and html attributes is not appealing to me. I wanted to replace a complex web framework with a simple templating engine. Thymeleaf does not seem simple to me 🤷. For the record this is what Thymeleaf looks like:
1<table>
2 <thead>
3 <tr>
4 <th th:text="#{msgs.headers.name}">Name</th>
5 <th th:text="#{msgs.headers.price}">Price</th>
6 </tr>
7 </thead>
8 <tbody>
9 <tr th:each="prod: ${allProducts}">
10 <td th:text="${prod.name}">Oranges</td>
11 <td th:text="${#numbers.formatDecimal(prod.price, 1, 2)}">0.99</td>
12 </tr>
13 </tbody>
14</table>
Freemarker
The last templating Engine proposed by the excellent Spring Initializer was Freemarker.
It is elegant, clearly distinguishes between templating language and template contents.
With macros there is also the possibility to create
reusable components.
And getting started is simple as well.
Just include the spring-boot-starter-freemarker
dependency.
The same layout from handlebars above looks like this in freemarker:
1<#macro layout nav content footer="Footer default"> 2 <div> 3 <div class="nav"> 4 ${nav} 5 </div> 6 <div class="content"> 7 ${content} 8 </div> 9 ${footer} 10 </div> 11</#macro> 12 13<@layout nav="My Nav" content="My content"></@layout>
I like this engine more than handlebars because it is more versatile (multiple macros per file) and is also cleaner to read.
HTML to the browser
So now that we know how we want to generate our HTML let's have a look how we bring this HTML in the browser of a user. We create a new project using the Spring initializer containing the following packages:
- spring-web
- spring-freemarker
- dev-tools (for hot reloading)
Next we create the following index.tlfh
file in src/main/resources/templates
:
1<!doctype html>
2<html lang="en">
3
4<head>
5 <meta charset="utf-8"/>
6 <meta name="viewport" content="width=device-width, initial-scale=1">
7
8 <title>SSR Spring boot demo</title>
9</head>
10
11<body>
12<div>
13 <h1> Hello from Freemarker</h1>
14</div>
15</body>
Run ./gradlew booRun
and go to http://localhost:8080 to see our beautiful first rendered page.
Note: The default file extension for freemarker is .ftl
.
Spring uses the sensible extensions .ftlh
which tells freemarker to escape HTML passed via a model.
Now to make it less bland I (as a backend developer) add bootstrap 😉.
It is up to you to choose tailwind, bulma, materialUI or something completely different.
Because I do want to have this whole app as simple to maintain as possible,
I added bootstrap as webjar.
This way the bootstrap dependency version is discoverable via the build.gradle
file and by extensions also by tools
like renovate or dependabot.
This leads to easier version updates.
The alternative would be to have tags like this
1<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" 2 integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
with fixed versions somewhere deep inside some template file and I don't want this.
To include the webjars we need to add dependencies org.webjars:bootstrap:5.3.3
and org.webjars:webjars-locator:0.52
to our build.gradle
.
The first one contains the build artefacts of this version.
The webjar-locator
is responsible to resolve the path /webjars/bootstrap/css/bootstrap.min.css
to the actual file
stored in the dependency.
Our "styled" index looks like this
1<!doctype html>
2<html lang="en">
3
4<head>
5 <meta charset="utf-8"/>
6 <meta name="viewport" content="width=device-width, initial-scale=1">
7
8 <title>SSR Spring boot demo</title>
9
10 <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet">
11 <script src="/webjars/bootstrap/js/bootstrap.bundle.min.js"></script>
12</head>
13
14<body>
15<div>
16 <h1> Hello from Freemarker</h1>
17</div>
18</body>
Let's add some "real" functionality to this App. We want to have a product overview and a product detail page. We implement a controller and two fragments for the overview and the product detail page.
1@Controller
2class ProductsController {
3
4 val products = listOf(Product(id = "one"), Product())
5
6 @GetMapping("/products")
7 fun getProducts(): ModelAndView {
8 return ModelAndView("sites/products", mapOf("products" to products))
9 }
10
11 @GetMapping("/products/{id}")
12 fun getProduct(@PathVariable id: String): ModelAndView {
13 return ModelAndView("sites/product", mapOf("product" to getProductById(id)))
14 }
15}
First we define a custom macro named page
with <#macro page>
.
This macro contains the repeating HTML boilerplate.
The macro is located in the file fragments/shared-components.ftlh
.
The <#nested/>
directive will expand to whatever is contained in the macro on execution.
1<#macro page>
2<!doctype html>
3<html lang="en">
4
5<head>
6 <meta charset="utf-8"/>
7 <meta name="viewport" content="width=device-width, initial-scale=1">
8
9 <title>SSR Spring boot demo</title>
10
11 <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet">
12 <script src="/webjars/bootstrap/js/bootstrap.bundle.min.js"></script>
13</head>
14
15<body>
16<#--SNIP Code for navigation bar-->
17<div class="container my-5">
18 <div id="container">
19 <#nested/>
20 </div>
21</div>
22</body>
23</html>
24</#macro>
Now we define the sites themselves.
The anatomy of these freemarker templates contain different interesting parts.
The most straightforward one is ${product.name}
.
With this syntax we are reading the value of name
of the model property product
.
The next one is the #import
directive. It makes macros and functions
defined in another file available within a custom namespace (com
in our case).
With <@com.page>
we are executing this macro.
The product detail page looks like this.
1<#import "../fragments/shared-components.ftlh" as com> 2 3<@com.page> 4<h2>Single Product</h2> 5 6<h3>${product.name}</h3> 7<p>${product.desc}</p> 8<p>${product.details}</p> 9<p>${product.id}</p> 10</@com.page>
There are other directives as well. With <#list products as p>
we are invoking
the list
directive of freemarker to iterate over the
list stored in the model property products
.
1<#import "../fragments/shared-components.ftlh" as com> 2 3<@com.page> 4<h2>Products</h2> 5 6 <#list products as p> 7 <div class="card mb-3"> 8 <div class="card-body"> 9 <h5 class="card-title">${p.name}</h5> 10 <h6 class="card-subtitle mb-2 text-muted">Id: ${p.id}</h6> 11 <p class="card-text">${p.desc}</p> 12 <a href="/products/${p.id}" class="card-link">Details</a> 13 </div> 14 </div> 15 </#list> 16</@com.page>
With the omitted navigation bar the final rendered results are looking like this:
Products overview
Product detail view
Interactivity
This looks promising, but why does it feel like the beginning of the internet again? On every click the whole page does a full reload including the header and all the resources. Also, the only loading indicator is this small icon in the browser tab. Of course for this app the loading times are trivial as we are effectively reading some data from memory and sending it over a zero latency connection to the browser. But let's assume this connection is slow and the business logic is complex. So lets make this page "modern" and snappy again.
HTMX to the rescue.
The basic principle of HTMX is to replace a certain part of the
website with content received from the server. Traditionally, when you click
on a link the server sends the whole updated website over the wire, and the
browser parses and renders it again from scratch. With HTMX we can tell
HTMX via the hx-target
attribute on the link to just replace the
contents of the referenced html element with the response from the
webserver. This way the browser only needs to rerender this div. Also, we
can also do nice things like displaying a custom loading spinner.
So how do I do this?
My app now has like two hundred links, do I really need to add these magical hx-
attributes to all those links to
enable them for HTMX?
No - thankfully there is an even more magical attribute called hx-boost
. If
this attribute is
present on any parent element of an anchor (<a>
) tag HTMX automagically does its thing.
To use HTMX we first need to add the webjar dependency org.webjars.npm:htmx.org:1.9.11
to our build.gradle
and
include it in the header of our page macro (<script src="/webjars/htmx.org/dist/htmx.min.js"></script>
).
Lastly add hx-boost
to the body of the page template.
1<body 2 hx-target="#container" 3 hx-boost="true" 4>
This snippet tells HTMX to replace the contents of the element with the id container
with the response from
the server. Not only ids are supported but any CSS selectors.
So when we now click on the link to the product detail page HTMX replaces only the inner html of the container
div
with the result from the server.
Insert facepalm gif
The content div now contains the full website again, including the navbar. What went wrong?
Well our backend does not know anything about this partial update mechanism and happily sends the whole template rendered again when prompted. This leads to the fail you see above.
So how do we only send partial updates when necessary?
When HTMX makes requests it adds certain headers. On the backend side of things we can simply render
different templates depending on the presence of the most basic header
(HX-Request=true
). HTMX also sends a bunch of other
headers as well denoting the target element and other meta
information, so the opportunity exists to make this as complex as possible needed.
In code this looks like this:
1@GetMapping("/products")
2fun getProducts(): ModelAndView {
3 return ModelAndView("sites/products", mapOf("products" to products))
4}
5
6@GetMapping("/products", headers = ["HX-Request=true"])
7fun getProductsHtmx(): ModelAndView {
8 return ModelAndView("fragments/products", mapOf("products" to products))
9}
With the fragment (fragments/products.fthl
) looking like this:
1<h2>Products</h2> 2 3<#list products as p> 4<div class="card mb-3"> 5 <div class="card-body"> 6 <h5 class="card-title">${p.name}</h5> 7 <h6 class="card-subtitle mb-2 text-muted">Id: ${p.id}</h6> 8 <p class="card-text">${p.desc}</p> 9 <a href="/products/${p.id}" class="card-link">Details</a> 10 </div> 11</div> 12</#list>
The full page (sites/products.ftlh
) simply includes the fragment within the page macro:
1<#import "../fragments/shared-components.ftlh" as com>
2
3<@com.page>
4 <#include "../fragments/products.ftlh">
5</@com.page>
The hx-boost
attribute also changes the address bar to the URL which was called.
When correctly implemented on the server side, this leads to identical DOMs on page reload or bookmarks.
To better indicate an ongoing request we use the hx-indicator
. This signals to HTMX that it should add a CSS class to an
element when requests are ongoing. In our case this simply leads to a spinner element of bootstrap to be shown.
See global.css
for the extensive implementation 😉. This class could be used to implement any progress
indicator CSS is capable of. And if your want to do something CSS is incapable of you can use
the events provided by HTMX as an escape hatch.
Error handling
When a HTMX boosted request fails for any reason, by default, nothing visible happens. No DOM update, no toast. Only a message in the browser console that a certain request failed. To handle this case gracefully, HTMX provides the response targets extension. This extension allows us to use the response of any failing request and render it at any place in the DOM. Just like the normal HTMX but for error responses instead.
To use it we have to include another script in our page macro. The extension is distributed with HTMX so we do not need another webjar dependency.
We can simply add the script tag <script src="/webjars/htmx.org/dist/ext/response-targets.js"></script>
.
We also need to explicitly enable the extension and tell it where to render our error responses.
To do this we add some more additional attributes to the body of our page.
The new body tag now looks like this:
1<body 2 hx-target="#container" 3 hx-boost="true" 4 hx-indicator="#spinner" 5 hx-ext="response-targets" 6 hx-target-error="#any-errors" 7>
As we are doing server side rendering this error messages have to be rendered on the server as well. In this case we simply use the bootstrap toasts to display a short summary of the error. This is done in this Controller
1@ControllerAdvice
2class MyErrorController {
3
4 @ExceptionHandler(Exception::class)
5 fun handleExceptions(
6 e: Exception,
7 request: HttpServletRequest,
8 response: HttpServletResponse
9 ): ModelAndView {
10 val model: MutableMap<String, Any?> = mutableMapOf(
11 "message" to e.message,
12 "exception" to mapOf(
13 "message" to e.message,
14 "stacktrace" to e.stackTraceToString()
15 )
16 )
17 val statusCode: HttpStatusCode = (if (e is ErrorResponse) e.statusCode else HttpStatus.INTERNAL_SERVER_ERROR)
18
19 model["message"] = "Request to ${request.requestURI} failed with Code $statusCode"
20
21 if (request.getHeader("HX-Request") == "true") {
22 response.addHeader("HX-Reswap", "beforeend")
23 response.addHeader("HX-Push-Url", "false")
24 return ModelAndView("fragments/error", model, statusCode)
25 }
26 return ModelAndView("sites/error", model, statusCode)
27 }
28}
This controller defines the global exception handler for all types of exception.
First, it extracts various values from the exception and the request and builds the model with it.
As our usual pattern we decide if this is an HTMX request based on the header HX-Request
.
If not, a simple page is rendered from the model.
But if the request is an HTMX request, we modify the behaviour of HTMX by setting certain response headers.
First with HX-Reswap=beforeend
we tell HTMX to not replace the content of the defined error target, but instead append
the returned HTML at the end of the already present inner HTML.
This way the errors are stacking in the toast container until the user dismisses them.
With the second header (HX-Push-Url=false
), we tell HTMX to not push the URL to the browser address bar and history.
This is done to keep the view in sync with the URL.
We do not replace the content of the page, so the URL should stay the same as well.
To demonstrate this behaviour I added some more entries to the navigation bar.
- The "Graceful error" renders a specific error template from within the controller. This could be used for specific business errors only happening there.
- The "Uncaught exception" throws a not
NotImplementedError
. This path leads to the ErrorController above, which maps this exception to Status code 500 and renders the generic template. - The "Not found" item maps to no controller or resource at all. This prompts Spring to throw
a
NoResourceFoundException
which is handled by the error controller as well. This class implements theErrorResponse
interface and therefore provides its own status code (404).
The resulting toasts look like this:
What about more interactivity?
Of course, JavaScript is not forbidden just because you started your application in the way described in this post. In this sample I update the highlighted nav-bar element via a simple hook, whenever either a page loads or a HTMX request occurs. Of course, this could also be achieved with server-side rendering, but why make it complicated if six lines of JavaScript are sufficient. See the file navbar-highlighting.js for the implementation details.
Ok, simple JavaScript snippets are possible, but what if my application has this one workflow
where there is a lot of user interactivity needed?
Am I now cursed to abstain from JavaScript-frameworks altogether and implement everything with plain and painful JavaScript like
in the olden days?
No! Of course, it is possible to integrate a classic single page app with this setup.
I choose a vue application, but I am sure you can also transfer the important bits to your js framework of the year
month week.
The only requirement is that it can be built to static files. So no Next.js et al.
For embedding this vue app into the otherwise pre rendered templates there are three magic ingredients:
A
index.html
containing only the import of the main App. For a production build this renders to only the script bundle and the CSS bundle. See vite.config.ts for how it is done exactly.A simple post build script which copies the files from the frontend build directory to the backend resources directory at the correct place.
The
main.ts
of the vue app only mounts the app when an event is received. This event is fired when the vue app fragment is rendered. This way we can define the time to render the app from the Spring Boot backend.
Lastly we include the necessary imports in our page macro and build the fragments to render the vue app on a specific path. No magic there. I also added a navigation bar entry to navigate to the page rendering the vue app.
More on how to model the data flows between the vue app and the backend will be shown in a later blogpost.
Next Steps
We are now at the end of what I wanted to show you today. Possible next steps are:
Deployment: Currently the app only runs locally. In order to be useful at all, we need to deploy it, so users can reach it. More about this in this Blogpost
Improved embedded vue app. Currently, the vue app does not interact with the spring boot app at all. A mechanism to pass data to and from the vue app has to be developed. There is also a follow-up post planned for this.
Testing: The current testing setup is lacking and also only works locally. For productive use, all implemented endpoints have to be tested.
Persistence: As you have probably already noticed, this app does not provide any means to add products. As our lambda has no way to store state between requests, this storage has to implemented using external mechanisms like a database or some files in S3. As a first stop I would try an AWS Dynamo simple table.
Summary
It is absolutely possible to create a responsive and partially updating web application with Spring Boot and HTMX. By the nature of using serverside rendering, a lot of business and display logic actually happens on the backend. This must suit you if you want to enjoy this style of application. It sure suits me.
The old "Right tool for the right job" proverb still holds true.
HTMX can enable you to shift a lot of logic to the backend and make the frontend dumber simpler.
But for some functions and use cases, JavaScript is simply the better tool.
As we saw this does not necessarily require a full blown frontend app.
We can use a JavaScript framework where it makes sense and stay with a simple serverside rendered app where not.
More articles
fromMirco Hacker
Your job at codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
More articles in this subject area
Discover exciting further topics and let the codecentric world inspire you.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog author
Mirco Hacker
IT Consultant
Do you still have questions? Just send me a message.
Do you still have questions? Just send me a message.