Spring Boot htmx Thymeleaf - CRUD Todo App
In this section, we will create a Todo CRUD app with Spring Boot, htmx, Thymeleaf, Bootstrap, and PostgreSQL.
About htmx
HTMX can be used for the API/server-side calls directly in the HTML.
More Info: click here
We can use HTMX to create interactive templates in our Spring Boot application. We can dynamically call and fetch data from the server by using simple HTML attributes like hx-get, hx-put, etc. We'll cover those in this article.
We will be exploring the basis of HTMX by creating a basic CRUD application.
Technologies Used
- Java 17
- Spring Boot 3.3.0
- Spring Data JPA
- PostgreSQL
- Thymeleaf
- htmx
- Bootstrap
Creating a spring boot application
First, open the Spring initializr https://start.spring.io
Then, Provide the Group and Artifact name. We have provided Group name com.knf.dev.demo and Artifact spring-boot-htmx-crud. Here I selected the Maven project - language Java 17 - Spring Boot 3.3.0, Spring Web, Spring Data JPA, Lombok, PostgreSQL Driver, and Thymeleaf.
Add htmx and Bootstrap WebJars.
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>5.3.3</version>
</dependency>
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>htmx.org</artifactId>
<version>1.9.12</version>
</dependency>
Final Project Directory
Complete pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.knf.dev.demo</groupId>
<artifactId>spring-boot-htmx-crud</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-htmx-crud</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>5.3.3</version>
</dependency>
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>htmx.org</artifactId>
<version>1.9.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yaml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/postgres
username: postgres
password: root
jpa:
hibernate:
ddl-auto: update # Hibernate ddl auto (create, create-drop, validate, update),production set to none or comment it
Spring Data JPA – Todo Entity
package com.knf.dev.demo.entity;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Todo {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@Column(name = "fname")
private String firstName;
@Column(name = "lname")
private String lastName;
private String email;
private String planet;
}
Spring Data JPA – Todo Repository
package com.knf.dev.demo.repository;
import com.knf.dev.demo.entity.Todo;
import org.springframework.data.jpa.repository.JpaRepository;
public interface TodoRepository extends JpaRepository<Todo,Long> {
}
Create a Spring MVC Todo Controller
package com.knf.dev.demo.controller;
import com.knf.dev.demo.entity.Todo;
import com.knf.dev.demo.repository.TodoRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Controller
@RequiredArgsConstructor
public class TodoController {
private final TodoRepository todoRepository;
@GetMapping("/")
public String index() {
return "index";
}
@GetMapping("/create")
public String createTodoView() {
return "create";
}
@PostMapping("/create")
public String createTodo(Todo todo, Model model) {
todoRepository.save(todo);
List<Todo> list = todoRepository.findAll();
model.addAttribute("todos", list);
return "read";
}
@GetMapping("/read")
public String listTodo(Model model) {
List<Todo> list = todoRepository.findAll();
model.addAttribute("todos", list);
return "read";
}
@DeleteMapping("/delete/{id}")
public String deleteTodo(@PathVariable Long id, Model model) {
todoRepository.deleteById(id);
List<Todo> list = todoRepository.findAll();
model.addAttribute("todos", list);
return "read";
}
@GetMapping("/update/{id}")
public String updateTodoView(@PathVariable Long id, Model model) {
Todo todo = todoRepository.findById(id).get();
model.addAttribute("todo", todo);
return "update";
}
@PutMapping("/update/{id}")
public String updateTodo(@PathVariable Long id, Model model, Todo t) {
Todo todo = todoRepository.findById(id).get();
todo.setPlanet(t.getPlanet());
todo.setEmail(t.getEmail());
todo.setFirstName(t.getFirstName());
todo.setLastName(t.getLastName());
todoRepository.save(todo);
List<Todo> list = todoRepository.findAll();
model.addAttribute("todos", list);
return "read";
}
}
Create index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Todo CRUD</title>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link th:href="@{/webjars/bootstrap/5.3.3/css/bootstrap.min.css}"
rel="stylesheet"/>
<script th:src="@{/webjars/bootstrap/5.3.3/js/bootstrap.bundle.min.js}"
defer></script>
<script th:src="@{/webjars/htmx.org/1.9.12/dist/htmx.min.js}" defer></script>
</head>
<body>
<h1 class="text-center">Spring Boot + HTMX + Thymeleaf: Todo CRUD App</h1>
<div class="text-center">
<button
type="button"
class="btn btn-primary"
hx-get="/create"
hx-target="#target">
Add
</button>
<button
type="button"
class="btn btn-info"
hx-get="/read"
hx-target="#target">
View
</button>
</div>
<div class="target" id="target"></div>
</body>
</html>
On click the button, htmx will call the corresponding endpoint via Ajax and replace our targeted <div> with the response.
Here are explanations for the HTMX attributes:
- hx-get: The hx-get attribute will cause an element to issue a GET to the specified URL.
- hx-target: identifies the target HTML element to receive response content.
Create create.html
<div class="container mt-5">
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<form
hx-post="/create"
method="post"
hx-target="#target">
<h2 class="">Add Todo</h2>
<!-- First Name -->
<div class="form-group col-md-6">
<label class="col-form-label"> First Name
</label>
<input
class="form-control"
type="text"
id="firstName"
name="firstName"
required/>
</div>
<!-- Last Name -->
<div class="form-group col-md-6">
<label class="col-form-label"> Last Name
</label>
<input
class="form-control"
type="text"
id="lastName"
name="lastName"
required/>
</div>
<!-- Email -->
<div class="form-group col-md-6">
<label class="col-form-label"> Email
</label>
<input
class="form-control"
type="text"
id="email"
name="email"
required/>
</div>
<!-- Planet -->
<div class="form-group col-md-6">
<label class="col-form-label"> Planet
</label>
<input
class="form-control"
type="text"
id="planet"
name="planet"
required/>
</div>
<br>
<!-- submit -->
<div class="form-group col-md-6">
<div class="text-center">
<input type="submit" class="btn btn-success"
value=" Submit ">
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
Here are explanations for the HTMX attributes:
- hx-post: The hx-get attribute will cause an element to issue a POST to the specified URL.
- hx-target: identifies the target HTML element to receive response content.
Create read.html
<div class="container mt-4">
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
<th>Planet</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr th:if="${todos.isEmpty()}">
<td class="text-center" colspan="5">No Records found. Add some...!</td>
</tr>
<tr th:each="todo : ${todos}">
<td th:text="${todo.firstName}"></td>
<td th:text="${todo.lastName}"></td>
<td th:text="${todo.email}"></td>
<td th:text="${todo.planet}"></td>
<td>
<button
type="button"
class="btn btn-warning"
th:hx-get="'/update/' + ${todo.id}"
hx-target="#target">
Edit
</button>
<button
type="button"
class="btn btn-danger"
th:hx-delete="'/delete/' + ${todo.id}"
hx-confirm="Are you sure?"
hx-target="#target">
Delete
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
Here are explanations for the HTMX attributes:
- hx-delete: The hx-get attribute will cause an element to issue a DELETE to the specified URL.
- hx-target: identifies the target HTML element to receive response content.
- hx-confirm: hx-confirm attribute allows you to confirm an action before issuing a request.
- hx-get: The hx-get attribute will cause an element to issue a GET to the specified URL.
Create update.html
<div class="container mt-5">
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<form
th:hx-put="'/update/' + ${todo.id}"
method="put"
hx-target="#target">
<h2 class="">Update Todo</h2>
<!-- First Name -->
<div class="form-group col-md-6">
<label class="col-form-label"> First Name
</label>
<input
class="form-control"
type="text"
id="firstName"
name="firstName"
th:value="${todo.firstName}"
required/>
</div>
<!-- Last Name -->
<div class="form-group col-md-6">
<label class="col-form-label"> Last Name
</label>
<input
class="form-control"
type="text"
id="lastName"
name="lastName"
th:value="${todo.lastName}"
required/>
</div>
<!-- Email -->
<div class="form-group col-md-6">
<label class="col-form-label"> Email
</label>
<input
class="form-control"
type="text"
id="email"
name="email"
th:value="${todo.email}"
required/>
</div>
<!-- Planet -->
<div class="form-group col-md-6">
<label class="col-form-label"> Planet
</label>
<input
class="form-control"
type="text"
id="planet"
name="planet"
th:value="${todo.planet}"
required/>
</div>
<br>
<!-- submit -->
<div class="form-group col-md-6">
<div class="text-center">
<input type="submit" class="btn btn-success"
value=" Submit ">
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
Here are explanations for the HTMX attributes:
- hx-put: The hx-get attribute will cause an element to issue a PUT to the specified URL.
- hx-target: identifies the target HTML element to receive response content.
Run the application - SpringBootHtmxCrudApplication.java
package com.knf.dev.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootHtmxCrudApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootHtmxCrudApplication.class, args);
}
}
Run the Spring Boot application - mvn spring-boot:run
OR
Run this Spring boot application from
- IntelliJ IDEA IDE by right click - Run 'Application.main()'
- Eclipse/STS - You can right click the project or the Application.java file and run as java application or Spring boot application.
Run the Spring Boot application - mvn spring-boot:run
OR
Run this Spring boot application from
- IntelliJ IDEA IDE by right click - Run 'Application.main()'
- Eclipse/STS - You can right click the project or the Application.java file and run as java application or Spring boot application.
Access URL via browser: http://localhost:8080