Spring Boot Thymeleaf CRUD Example Tutorial


In this tutorial, we’ll learn how to develop a CRUD web application with Spring Boot and Thymeleaf.

Video Tutorial

This tutorial is very well explained in below YouTube video. Subscribe to our youtube channel for future updates at https://www.youtube.com/c/javaguides.

What we'll Build?

We are building a CRUD Web application for the Student resource (create student, list students, update student and delete student) using Spring boot and Thymeleaf. Let's use an H2 database for quick setup and running this web application.

Tools and Technologies Used

  • Spring Boot - 3+
  • Spring Framework - 6+
  • Hibernate - 6+
  • Thymeleaf - 3+
  • Maven - 3.2+
  • IDE - Eclipse or Spring Tool Suite (STS)
  • H2 Database

Development Steps

  1. Creating a Spring Boot Application
  2. Maven Dependencies - Pom.xml
  3. Project Structure
  4. Domain Layer
  5. The Repository Layer
  6. The Controller Layer
  7. The View Layer
  8. Running the Application
  9. Demo
  10. Conclusion

1. Creating a Spring Boot Application

First, create a Spring Boot project using the Spring Initializr or your preferred IDE.
There are many ways to create a Spring Boot application. You can refer below articles to create a Spring Boot application.
>> Create Spring Boot Project With Spring Initializer
>> Create Spring Boot Project in Spring Tool Suite [STS] 

2. Maven Dependencies - pom.xml

Add the following dependencies to your pom.xml file:
<?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.0.4</version>
        <relativePath/>
        <!-- lookup parent from repository -->
    </parent>
    <groupId>net.javaguides.springboot</groupId>
    <artifactId>springboot-thymeleaf-crud-tutorial</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-thymeleaf-crud-tutorial</name>
    <description>springboot-thymeleaf-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>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </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>

3. Project Structure

Following is the package or project structure for your reference -

4. Create JPA Entity

Let's create a JPA Student entity with the following fields:
package net.javaguides.springboot.tutorial.entity;

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

@Entity
public class Student {

    @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 = "phone_no")
    private long phoneNo;

    public Student() {}

    public Student(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 long getPhoneNo() {
        return phoneNo;
    }

    public void setPhoneNo(long phoneNo) {
        this.phoneNo = phoneNo;
    }
}

5. The Repository Layer

Create a StudentRepository interface that extends the JpaRepository interface:
package net.javaguides.springboot.tutorial.repository;

import java.util.List;

import org.springframework.data.repository.JpaRepository;

import net.javaguides.springboot.tutorial.entity.Student;

public interface StudentRepository extends CrudRepository <Student, Long> { 
    List<Student> findByName(String name); 
}
This interface defines a data access object (DAO) for the Student entity, providing CRUD (Create, Read, Update, and Delete) operations for the entity.

The StudentRepository interface also defines a custom query method called findByName(String name), which returns a list of Student entities that match the given name. 

6. The Controller Layer or Web Layer

Let's create a StudentController class with following methods:
package net.javaguides.springboot.tutorial.controller;

import javax.validation.Valid;

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 net.javaguides.springboot.tutorial.entity.Student;
import net.javaguides.springboot.tutorial.repository.StudentRepository;

@Controller
@RequestMapping("/students/")
public class StudentController {

    private final StudentRepository studentRepository;

    @Autowired
    public StudentController(StudentRepository studentRepository) {
        this.studentRepository = studentRepository;
    }

    @GetMapping("signup")
    public String showSignUpForm(Student student) {
        return "add-student";
    }

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

    @PostMapping("add")
    public String addStudent(@Valid Student student, BindingResult result, Model model) {
        if (result.hasErrors()) {
            return "add-student";
        }

        studentRepository.save(student);
        return "redirect:list";
    }

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

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

        studentRepository.save(student);
        model.addAttribute("students", studentRepository.findAll());
        return "index";
    }

    @GetMapping("delete/{id}")
    public String deleteStudent(@PathVariable("id") long id, Model model) {
        Student student = studentRepository.findById(id)
            .orElseThrow(() - > new IllegalArgumentException("Invalid student Id:" + id));
        studentRepository.delete(student);
        model.addAttribute("students", studentRepository.findAll());
        return "index";
    }
}

7. The View Layer

Under the src/main/resources/templates folder, we need to create the HTML templates required for displaying the Student signup student, update student, and rendering the list of persisted Student entities. We will be using Thymeleaf as the underlying template engine for parsing the template files. 

add-student.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 Student</h3>
        <div class="card">
            <div class="card-body">
                <div class="col-md-10">
                    <form action="#" th:action="@{/students/add}" th:object="${student}" 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="phoneNo" class="col-form-label">Phone No</label> <input type="text" th:field="*{phoneNo}" class="form-control" id="phoneNo" placeholder="PhoneNo"> <span th:if="${#fields.hasErrors('phoneNo')}" th:errors="*{phoneNo}" class="text-danger"></span>
                            </div>

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

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

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

</html>

List of Students - 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="${students}" class="container my-5">
                    <p class="my-5">
                        <a href="/students/signup" class="btn btn-primary"><i
       class="fas fa-user-plus ml-2"> Add Student</i></a>
                    </p>
                    <div class="col-md-10">
                        <h2 th:case="null">No Students yet!</h2>
                        <div th:case="*">
                            <table class="table table-striped table-responsive-md">
                                <thead>
                                    <tr>
                                        <th>Name</th>
                                        <th>Email</th>
                                        <th>Phone No</th>
                                        <th>Edit</th>
                                        <th>Delete</th>
                                    </tr>
                                </thead>
                                <tbody>
                                    <tr th:each="student : ${students}">
                                        <td th:text="${student.name}"></td>
                                        <td th:text="${student.email}"></td>
                                        <td th:text="${student.phoneNo}"></td>
                                        <td><a th:href="@{/students/edit/{id}(id=${student.id})}" class="btn btn-primary"><i class="fas fa-user-edit ml-2"></i></a></td>
                                        <td><a th:href="@{/students/delete/{id}(id=${student.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 Student - update-student.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 Student</h3>
        <div class="card">
            <div class="card-body">
                <div class="col-md-8">
                    <form action="#" th:action="@{/students/update/{id}(id=${student.id})}" th:object="${student}" 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="phoneNo" class="col-form-label">Phone No</label> <input type="text" th:field="*{phoneNo}" class="form-control" id="phoneNo" placeholder="phoneNo"> <span th:if="${#fields.hasErrors('phoneNo')}" th:errors="phoneNo" class="text-danger"></span>
                            </div>

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

</html>

8. Running the Application

Let's run Spring boot application from main entry point class:
package net.javaguides.springboot.tutorial;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

9. Demo

Let's access the above-deployed web application using http://localhost:8080 Initially, you don't have any students in the list so let's add a new student first. 

Add Student Screen

List of Students Screen

Update Student Screen

Delete Student Screen

10. Conclusion

In this tutorial, we learned how to build a basic CRUD web application with Spring Boot and Thymeleaf.

GitHub Repository

The source code of this tutorial is available on my GitHub Repository.

Comments

  1. You can download source code of this tutorial from my GitHub repository, the link has given at end of this tutorial.

    ReplyDelete
  2. please can you show us the "application.properties" file for this project (:

    ReplyDelete
    Replies
    1. I have configured default settings in this spring boot thymeleaf crud example tutorial so the application.properties file is not required. If you want to use MySQL database or other stuff to configure then you should use application.properties file. In this example tutorial, the application.properties file is empty. You can checkout source code of this tutorial at my GitHub repository.

      Delete
  3. Hi I am having an error like this:

    Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

    Reason: Failed to determine a suitable driver class


    Action:

    Consider the following:
    If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
    If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).

    ReplyDelete
  4. please provide database connection system of this project...

    ReplyDelete
    Replies
    1. We are using H2 database with default configuration in this tutorial so the application.properties file is not required. If you want to use MySQL database or other stuff to configure then you should use application.properties file. In this example tutorial, the application.properties file is empty. You can checkout source code of this tutorial at my GitHub repository.

      Delete
  5. I am using Mysql so I added below details in application.properties

    spring.datasource.url = jdbc:mysql://localhost:3306/demo?useSSL=false
    spring.datasource.username = root
    spring.datasource.password = root

    spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect

    spring.jpa.hibernate.ddl-auto = update
    spring.jpa.show-sql=true

    But I am facing this issue

    Description:

    Failed to bind properties under '' to com.zaxxer.hikari.HikariDataSource:

    Property: driverclassname
    Value: com.mysql.cj.jdbc.Driver
    Origin: "driverClassName" from property source "source"
    Reason: Failed to load driver class com.mysql.cj.jdbc.Driver in either of HikariConfig class loader or Thread context classloader

    Action:

    Update your application's configuration

    ReplyDelete
    Replies
    1. We are using H2 database with default configuration in this tutorial so the application.properties file is not required.
      If you want to use MySQL database then add MySQL driver dependency to pom.xml.

      Delete
  6. i am not able to build this project

    ReplyDelete
    Replies
    1. i am sorry .. i am able to build now..
      i was getting this error "hibernate-commons-annotations-4.0.1.Final.jar; invalid LOC header (bad signature)"

      But when i remove all jars in my repository .. and updated again ..

      Delete
    2. good job ramesh .. i just found your web site .. it is very help fulll to upgrade my skills as full stack developer

      Delete
  7. I am getting login page.. what i do...???

    ReplyDelete
    Replies
    1. Remove Security dependency from your pom.xml or configure it.

      Delete

Post a Comment

Leave Comment