HTMX for Java with Spring Boot and Thymeleaf


Not long ago, we looked at how to build an HTMX application with JavaScript. HTMX also works with Java, so now we’ll try that out using Spring Boot and Thymeleaf. This awesome stack gives you all the power and versatility of Java with Spring, combined with the ingenious simplicity of HTMX.

HTMX: A rising star

HTMX is a newer technology that takes plain old HTML and gives it extra powers like Ajax and DOM swaps. It’s included in my personal list of good ideas because it eliminates a whole realm of complexity from the typical web app. HTMX works by converting back and forth between JSON and HTML. Think of it as a kind of declarative Ajax.

Read an interview with HTMX creator Carson Gross.

Java, Spring, and Thymeleaf

On the other side of this equation is Java: one of the most mature and yet innovative server-side platforms bar none. Spring is an easy choice for adding a range of Java-based capabilities, including the well-designed Spring Boot Web project for handling endpoints and routing. 

Thymeleaf is a complete server-side templating engine and the default for Spring Boot Web. When combined with HTMX, you have everything you need to build a full-stack web app without getting into a lot of JavaScript. 

HTMX and Java example app

We’re going to build the canonical Todo app. It will look like this:

Screenshot of the HTMX-Spring Java example app. Matthew Tyson

We list the existing to-do’s, and allow for creating new ones, deleting them, and changing their completion status.

Overview

This is what the finished Todo app looks like on disk:


$ tree
.
├── build.gradle
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
    └── main
        ├── java
        │   └── com
        │       └── example
        │           └── iwjavaspringhtmx
        │               ├── DemoApplication.java
        │               ├── controller
        │               │   └── MyController.java
        │               └── model
        │                   └── TodoItem.java
        └── resources
            ├── application.properties
            ├── static
            │   └── style.css
            └── templates
                ├── index.html
                ├── style.css
                └── todo.html

So, besides the typical Gradle stuff, the app has two main parts contained in the /src directory: The /main directory holds the Java code and /resources holds the properties file and two subdirectories with the CSS and Thymeleaf templates.

You can find the source for this project on its GitHub repo. To run it, go to the root and type $ gradle bootRun. You can then use the app at localhost:8080.

If you want to start the app from the ground up, you can begin with: $ spring init --dependencies=web,thymeleaf spring-htmx. That will install Thymeleaf and Spring Boot into a Gradle project.

The app is a normal Spring Boot application run by DemoApplication.java.

The Java Spring HTMX model class

Let’s begin by looking at our model class: com/example/iwjavaspringhtmx/TodoItem.java. This is the server-side model class that will represent a to-do. Here’s what it looks like:


public class TodoItem {
  private boolean completed;
  private String description;
  private Integer id;
  public TodoItem(Integer id, String description) {
    this.description = description;
    this.completed = false;
    this.id = id;
  }
  public void setCompleted(boolean completed) {
    this.completed = completed;
  }
  public boolean isCompleted() {
    return completed;
  }
  public String getDescription() {
    return description;
  }
  public Integer getId(){ return id; }
  public void setId(Integer id){ this.id = id; }
  @Override
  public String toString() {
    return id + " " + (completed ? "[COMPLETED] " : "[ ] ") + description;
  }
}

This is a simple model class with getters and setters. Nothing fancy, which is just what we want.

The Java Spring HTMX controller class

On the server, the controller is the boss. It accepts requests, orchestrates the logic, and formulates the response. In our case, we need four endpoints used for listing the items, changing their completion status, adding items, and deleting them. Here’s the controller class:


@Controller
public class MyController {

  private static List items = new ArrayList();
  static {
    TodoItem todo = new TodoItem(0,"Make the bed");
    items.add(todo);
    todo = new TodoItem(1,"Buy a new hat");
    items.add(todo);
    todo = new TodoItem(2,"Listen to the birds singing");
    items.add(todo);
  }

  public MyController(){ }

  @GetMapping("https://www.infoworld.com/")
  public String items(Model model) {
    model.addAttribute("itemList", items);
    return "index";
  }

  @PostMapping("/todos/{id}/complete")
  public String completeTodo(@PathVariable Integer id, Model model) {
    TodoItem item = null;
    for (TodoItem existingItem : items) {
      if (existingItem.getId().equals(id)) {
        item = existingItem;
        break; 
      }
    }
    if (item != null) {
      item.setCompleted(!item.isCompleted());
    }
    model.addAttribute("item",item);
    return "todo"; 
  }

  @PostMapping("/todos")
  public String createTodo(Model model, @ModelAttribute TodoItem newTodo) {
    int nextId = items.stream().mapToInt(TodoItem::getId).max().orElse(0) + 1;
    newTodo.setId(nextId);
    items.add(newTodo);
    model.addAttribute("item", newTodo);
    return "todo";
  }

  @DeleteMapping("/todos/{id}/delete")
  @ResponseBody
  public String deleteTodo(@PathVariable Integer id) {
    for (int i = 0;  i < items.size(); i++) {
      TodoItem item = items.get(i);
      if (item.getId() == id) {
        items.remove(i);
        break;
      }
    }
    return "";
  }
}

You’ll notice that I’ve just created a static List to hold the items in memory. In real life, we would use an external data store.

For this tour, there are a few additional points of interest.

First, the endpoints are annotated with @GetMapping, @PostMapping, and @DeleteMapping. This is how you map Spring Web paths to handlers. Each annotation corresponds to its HTTP method (GET, POST, DELETE).

Spring Boot also makes it easy to grab parameters off the path using argument annotation @PathParameter. So, for the path /todos/{id}/delete, @PathVariable Integer id will contain the value in the {id} part of the path.

In the case of the createTodo() method, the argument annotated @ModelAttribute TodoItem newTodo, will automatically take the POST body and apply its values to the newTodo object. (This is a quick and easy way to turn a form submit into a Java object.)

Next, we use the item IDs to manipulate the list of items. This is standard REST API fare.

There are two ways to send a response. If the @ResponseBody annotation is present on the method (like it is for deleteTodo()) then whatever is returned will be sent verbatim. Otherwise, the return string will be interpreted as a Thymeleaf template path (you’ll see those in a moment).

The Model model argument is special. It’s used to add attributes to the scope that is handed off to Thymeleaf. We can interpret the following items method as saying: Given a GET request to the root/path, add the items variable to the scope as “itemList” and render a response using the “index” template.


@GetMapping("https://www.infoworld.com/")
  public String items(Model model) {
    model.addAttribute("itemList", items);
    return "index";
  }

In cases where we’re handling an AJAX request sent from the front end by HTMX, the response will be used by the HTMX component to update the UI. We’ll get a good look at this in practice soon.

The Thymeleaf templates

Now let’s have a look at Thymeleaf’s index template. It lives in the /resources/templates/index.html file. Spring Boot maps the “index” string returned from the items() method to this file using conventions. Here’s our index.html template:




  
    
    Items List