Spring Boot htmx Thymeleaf - CRUD Todo App

In this section, we will create a Todo CRUD app with Spring Boot, htmx, ThymeleafBootstrap, 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

Let’s configure Spring Boot to use PostgreSQL as our data source. We are simply adding PostgreSQL database URL, username, and password in the src/main/resources/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

A Todo object as JPA 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

JpaRepository is a JPA-specific extension of Repository. It contains an API for basic CRUD operations and also API for pagination and sorting.
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

Let's prepare a simple template for our index page. 
We can use the @{/webjars/htmx.org/… path for static content, served from Webjar.
Path: src/main/resources/templates/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

Path: src/main/resources/templates/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

Path: src/main/resources/templates/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

Path: src/main/resources/templates/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.

Access URL via browser: http://localhost:8080

Source code - click here!

Popular posts from this blog

Learn Java 8 streams with an example - print odd/even numbers from Array and List

Java Stream API - How to convert List of objects to another List of objects using Java streams?

Registration and Login with Spring Boot + Spring Security + Thymeleaf

Java, Spring Boot Mini Project - Library Management System - Download

ReactJS, Spring Boot JWT Authentication Example

Top 5 Java ORM tools - 2024

Java - Blowfish Encryption and decryption Example

Spring boot video streaming example-HTML5

Google Cloud Storage + Spring Boot - File Upload, Download, and Delete