Search

Suggested keywords:
  • Java
  • Docker
  • Git
  • React
  • NextJs
  • Spring boot
  • Laravel

Build CRUD App using Spring Boot, JPA and Thymeleaf

  • Share this:

post-title

Spring Boot is one of the widely used Java framework. Thymeleaf is one of popular templating engine which generates HTML.

Let's learn how to build a CRUD web application with Spring Boot and Thymeleaf in this tutorial.

Creating a Spring Boot Application

Create a spring application by right-clicking in the package explorer and selecting New -> Spring Starter Project and specify the project details.

screenshot

Project Structure

The structure followed in the project is listed below

screenshot

Maven Dependencies - pom.xml

You need to add the following dependencies to 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 http://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.1.5</version>
                <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.example</groupId>
        <artifactId>SpringBoot-ThymeleafCrudTutorial</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>SpringBoot-ThymeleafCrudTutorial</name>
        <description>Crud Tutorial</description>
        <properties>
                <java.version>17</java.version>
        </properties>
        <dependencies>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-data-jpa</artifactId>
                </dependency>
                <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-devtools</artifactId>
                        <scope>runtime</scope>
                        <optional>true</optional>
                </dependency>
                <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-validation</artifactId>
           <version>2.4.1</version>
       </dependency>
                <dependency>
                        <groupId>com.h2database</groupId>
                        <artifactId>h2</artifactId>
                        <scope>runtime</scope>
                </dependency>
                <dependency>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                        <optional>true</optional>
                </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>
					<configuration>
						<image>
							<builder>paketobuildpacks/builder-jammy-base:latest</builder>
						</image>
						<excludes>
							<exclude>
								<groupId>org.projectlombok</groupId>
								<artifactId>lombok</artifactId>
							</exclude>
						</excludes>
					</configuration>
				</plugin>
			</plugins>
        </build>

</project>

Create JPA Entity

Create a JPA Candidate entity with these fields:

package com.application.thymeleafcrud.entity;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;

@Entity
public class Candidate {
   
   @Id
   @GeneratedValue(strategy = GenerationType.AUTO)
   private long id;
   
   @NotBlank(message = "Name is mandatory")
   @Column(name = "name")
   private String name;
   
   @NotBlank(message = "Email is mandatory")
   @Column(name = "email")
   private String email;

   @Column(name = "age")
   private Integer age;
   
   public Candidate() {}

   public Candidate(String name, String email) {
       this.name = name;
       this.email = email;
   }

   public void setId(long id) {
       this.id = id;
   }
   
   public long getId() {
       return id;
   }
   
   public void setName(String name) {
       this.name = name;
   }
   
   public void setEmail(String email) {
       this.email = email;
   }

   public String getName() {
       return name;
   }

   public String getEmail() {
       return email;
   }
   
   public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
         this.age = age;
    }
}

 

The Repository Layer

Create a CandidateRepository Interface that extends the JpaRepository Interface:

package com.application.thymeleafcrud.repository;

import java.util.List;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import com.application.thymeleafcrud.entity.Candidate;

@Repository
public interface CandidateRepository extends CrudRepository<Candidate, Long> {
   List<Candidate> findByName(String name);  
}

 

For the Candidate entity, this interface defines a data access object (DAO) that provides CRUD operations (Create, Read, Update, Delete).

The CandidateRepository interface also provides a custom query method: findByName(String name), which returns all Candidate entities with the specified name.

The Controller Layer or Web Layer

Create a CandidateController class with the following methods:

package com.application.thymeleafcrud.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.application.thymeleafcrud.entity.Candidate;
import com.application.thymeleafcrud.repository.CandidateRepository;

import jakarta.validation.Valid;

@Controller
@RequestMapping("/candidates")
public class CandidateController {

   private final CandidateRepository candidateRepository;

   @Autowired
   public CandidateController(CandidateRepository candidateRepository) {
       this.candidateRepository = candidateRepository;
   }

   @GetMapping("/signup")
   public String showSignUpForm(Candidate candidate) {
       return "add-candidate";
   }

   @GetMapping("/list")
   public String showUpdateForm(Model model) {
       model.addAttribute("candidates", candidateRepository.findAll());
       return "index";
   }

   @PostMapping("/add")
   public String addCandidate(@Valid Candidate candidate, BindingResult result, Model model) {
       
       if (result.hasFieldErrors("name") 
             || result.hasFieldErrors("email") 
             || result.hasFieldErrors("age")) {
           
             return "add-candidate";
       }

       candidateRepository.save(candidate);
       return "redirect:list";
   }

   @GetMapping("/edit/{id}")
   public String showUpdateForm(@PathVariable("id") long id, Model model) {
       
       Candidate candidate = candidateRepository.findById(id)
               .orElseThrow(() -> new IllegalArgumentException("Invalid candidate Id:" + id));
       model.addAttribute("candidate", candidate);
       return "update-candidate";
   }

   @PostMapping("/update/{id}")
   public String updateCandidate(@PathVariable("id") long id, 
       @Valid Candidate candidate, BindingResult result,
       Model model) {
       
       if (result.hasErrors()) {
           candidate.setId(id);
           return "update-candidate";
       }

       candidateRepository.save(candidate);
       model.addAttribute("candidates", candidateRepository.findAll());
       return "index";
   }

   @GetMapping("/delete/{id}")
   public String deleteCandidate(@PathVariable("id") long id, Model model) {
       Candidate candidate = candidateRepository.findById(id)
           .orElseThrow(() -> new IllegalArgumentException("Invalid candidate Id:" + id));
       candidateRepository.delete(candidate);
       model.addAttribute("candidates", candidateRepository.findAll());
       return "index";
   }
}

The View Layer

For the Candidate signup and update, as well as rendering the list of persisted Candidate entities, we need to create HTML templates under src/main/resources/templates. To parse the template files, we will use Thymeleaf as the underlying template engine.

add-candidate.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Add User</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet"
        href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
        integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
        crossorigin="anonymous">
<link rel="stylesheet"
        href="https://use.fontawesome.com/releases/v5.4.1/css/all.css"
        integrity="sha384-5sAR7xN1Nv6T6+dT2mhtzEpVJvfS3NScPQTrOxhwjIuvcA67KV2R5Jz6kr4abQsz"
        crossorigin="anonymous">
<!-- <link rel="stylesheet" href="../css/shards.min.css"> -->
</head>
<body>
	<div class="container my-5">
	<h3> Add Candidate</h3>
  <div class="card">
  	<div class="card-body">
    <div class="col-md-10">
    	<form action="#" th:action="@{/candidates/add}"
      	th:object="${candidate}" method="post">
      	<div class="row">
        <div class="form-group col-md-8">
        	<label for="name" class="col-form-label">Name</label> <input
          	type="text" th:field="*{name}" class="form-control" id="name"
          	placeholder="Name"> <span
          	th:if="${#fields.hasErrors('name')}" th:errors="*{name}"
          	class="text-danger"></span>
        </div>
        <div class="form-group col-md-8">
        	<label for="email" class="col-form-label">Email</label> <input
          	type="text" th:field="*{email}" class="form-control" id="email"
          	placeholder="Email"> <span
          	th:if="${#fields.hasErrors('email')}" th:errors="*{email}"
          	class="text-danger"></span>
        </div>

        <div class="form-group col-md-8">
        	<label for="age" class="col-form-label">Age</label> <input
          	type="number" th:field="*{age}" class="form-control"
          	id="age" placeholder="Age"> <span
          	th:if="${#fields.hasErrors('age')}" th:errors="*{age}"
          	class="text-danger"></span>
        </div>

        <div class="col-md-6">
        	<input type="submit" class="btn btn-primary" value="Add Candidate">
        </div>

        <div class="form-group col-md-8"></div>

      	</div>
    	</form>
    </div>
  	</div>
  </div>
	</div>
</body>
</html>

index.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Users</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet"
        href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
        integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
        crossorigin="anonymous">
<link rel="stylesheet"
        href="https://use.fontawesome.com/releases/v5.4.1/css/all.css"
        integrity="sha384-5sAR7xN1Nv6T6+dT2mhtzEpVJvfS3NScPQTrOxhwjIuvcA67KV2R5Jz6kr4abQsz"
        crossorigin="anonymous">
<!-- <link rel="stylesheet" href="../css/shards.min.css"> -->
</head>
<body>
 <div class="container my-2">
   <div class="card">
     <div class="card-body">
       <div th:switch="${candidates}" class="container my-5">

         <p class="my-5">
           <a href="/candidates/signup" class="btn btn-primary"><i
              class="fas fa-user-plus ml-2"> Add Candidate</i></a>
         </p>

     <div class="col-md-10">
        <h2 th:case="null">No Candidates yet!</h2>
        <div th:case="*">
        
        <table class="table table-striped table-responsive-md">
           <thead>
              <tr>
               <th>Name</th>
               <th>Email</th>
               <th>Age</th>
			   <th>Edit</th>
               <th>Delete</th>
             </tr>
          </thead>
          <tbody>
             <tr th:each="candidate : ${candidates}">
                 <td th:text="${candidate.name}"></td>
                  <td th:text="${candidate.email}"></td>
                  <td th:text="${candidate.age}"></td>
                  <td>
                     <a th:href="@{/candidates/edit/{id}(id=${candidate.id})}"
                       class="btn btn-primary"><i class="fas fa-user-edit ml-2"></i>
                     </a>
				  </td>
                  <td>
                    <a th:href="@{/candidates/delete/{id}(id=${candidate.id})}"
                          class="btn btn-primary"><i class="fas fa-user-times ml-2"></i>
                  </a></td>
              </tr>
           </tbody>
           </table>
       </div>
     </div>
    </div>
   </div>
  </div>
  </div>
</body>
</html>

update-candidate.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Update User</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet"
   href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
   integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
   crossorigin="anonymous">
<link rel="stylesheet"
   href="https://use.fontawesome.com/releases/v5.4.1/css/all.css"
   integrity="sha384-5sAR7xN1Nv6T6+dT2mhtzEpVJvfS3NScPQTrOxhwjIuvcA67KV2R5Jz6kr4abQsz"
   crossorigin="anonymous">
</head>
<body>
   <div class="container my-5">
   <h3> Update Candidate</h3>
      <div class="card">
         <div class="card-body">
            <div class="col-md-8">
               <form action="#"
                  th:action="@{/candidates/update/{id}(id=${candidate.id})}"
                  th:object="${candidate}" method="post">
                  <div class="row">
                <div class="form-group col-md-6">
                   <label for="name" class="col-form-label">Name</label> <input
                      type="text" th:field="*{name}" class="form-control" id="name"
                      placeholder="Name"> <span
                      th:if="${#fields.hasErrors('name')}" th:errors="*{name}"
                      class="text-danger"></span>
                </div>
                <div class="form-group col-md-8">
                   <label for="email" class="col-form-label">Email</label> <input
                      type="text" th:field="*{email}" class="form-control" id="email"
                      placeholder="Email"> <span
                      th:if="${#fields.hasErrors('email')}" th:errors="*{email}"
                      class="text-danger"></span>
                </div>
                <div class="form-group col-md-8">
                   <label for="age" class="col-form-label">Age</label> <input
                      type="number" th:field="*{age}" class="form-control"
                      id="age" placeholder="age"> <span
                      th:if="${#fields.hasErrors('age')}" th:errors="age"
                      class="text-danger"></span>
                </div>

                <div class="form-group col-md-8">
                   <input type="submit" class="btn btn-primary"
                      value="Update Candidate">
                </div>
                </div>
               </form>
            </div>
         </div>
      </div>
   </div>
</body>
</html>

 

Demo Screenshots

Now when we run the application we would be able to see our application live at port 9098

Landing Screen:

screenshot

Add Candidate Screen

screenshot

List of Candidates Screen

screenshot

Update Candidate Screen

screenshot

After Updation List of Candidates

screenshot

Delete Candidate Screen

screenshot

Conclusion

We learned how to build CRUD web applications with Spring Boot and Thymeleaf in this tutorial.

Source code available in Github

Muthu Annamalai

About author
Technical Writer | Pre-Final Year Student | Code & Community | Developer who works everyday to improve himself.