In this tutorial we will learn how to create an application based on the Spring MVC pattern, and the Template engine Thymeleaf to render the page View.
This tutorial will guide you through building a simple Spring Boot 3 MVC application. The application will manage customer records with the ability to add, update, list, and delete customers. The frontend will use Thymeleaf templates for rendering the views.
Prerequisites
- Java 17+
- Maven 3.6+
- An IDE like IntelliJ IDEA or Eclipse
- Basic knowledge of Spring Boot and Thymeleaf
Setting Up the Spring Boot MVC Project
Start by creating a new Spring Boot project using Spring Initializr or your IDE. Include the following dependencies:
- Spring Web: For building the web layer.
- Thymeleaf: For rendering the views.
- Spring Data JPA: For data persistence.
- H2 Database: For an in-memory database (you can replace this with MySQL, PostgreSQL, etc., if needed).
The Model
The Customer
class represents the customer entity. It is annotated with @Entity
to map it to the database table.
import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Entity @Data @NoArgsConstructor @AllArgsConstructor public class Customer { @Id @GeneratedValue private Long id; private String name; private String surname; private String email; }
The Repository
The repository interface extends JpaRepository
to provide CRUD operations on the Customer
entity.
import org.springframework.data.jpa.repository.JpaRepository; public interface CustomerRepository extends JpaRepository<Customer, Long> { }
The Service
The service layer encapsulates business logic. It uses the repository to interact with the database:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; import java.util.Optional; @Service public class CustomerServiceImpl implements ICustomerService { @Autowired private CustomerRepository repo; @Override public Customer save(Customer customer) { return repo.save(customer); } @Override public List<Customer> findAll() { return repo.findAll(); } @Override public Customer getCustomerById(Long id) { Optional<Customer> opt = repo.findById(id); if (opt.isPresent()) { return opt.get(); } else { throw new CustomerNotFoundException("Customer with Id : " + id + " Not Found"); } } @Override public void deleteCustomerById(Long id) { repo.delete(getCustomerById(id)); } @Override public void update(Customer customer) { repo.save(customer); } }
The Controller
The controller handles HTTP requests, interacts with the service layer, and returns the appropriate views.
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import java.util.List; @Controller @RequestMapping("/customer") public class CustomerController { @Autowired private ICustomerService service; @GetMapping("/") public String showHomePage() { return "homePage"; } @GetMapping("/register") public String showRegistration() { return "registerCustomerPage"; } @PostMapping("/save") public String saveCustomer(@ModelAttribute Customer customer, Model model) { Long id = service.save(customer).getId(); String message = "Record with id : '" + id + "' is saved successfully!"; model.addAttribute("message", message); return "registerCustomerPage"; } @GetMapping("/getAllCustomers") public String getAllCustomers(@RequestParam(value = "message", required = false) String message, Model model) { List<Customer> customers = service.findAll(); model.addAttribute("list", customers); model.addAttribute("message", message); return "allCustomersPage"; } @GetMapping("/edit") public String getEditPage(Model model, RedirectAttributes attributes, @RequestParam Long id) { try { Customer customer = service.getCustomerById(id); model.addAttribute("customer", customer); return "editCustomerPage"; } catch (CustomerNotFoundException e) { attributes.addAttribute("message", e.getMessage()); return "redirect:getAllCustomers"; } } @PostMapping("/update") public String updateCustomer(@ModelAttribute Customer customer, RedirectAttributes attributes) { service.update(customer); attributes.addAttribute("message", "Customer with id: '" + customer.getId() + "' is updated successfully!"); return "redirect:getAllCustomers"; } @GetMapping("/delete") public String deleteCustomer(@RequestParam Long id, RedirectAttributes attributes) { try { service.deleteCustomerById(id); attributes.addAttribute("message", "Customer with Id : '" + id + "' is removed successfully!"); } catch (CustomerNotFoundException e) { attributes.addAttribute("message", e.getMessage()); } return "redirect:getAllCustomers"; } }
The Views
Within the src/main/resources/templates
we will be adding the views for our Spring MVC application. We will add the following HTML pages:
├── resources/ │ ├── static/ │ │ │ └── css/ │ │ ├── templates/ │ │ │ ├── allCustomersPage.html │ │ │ ├── editCustomerPage.html │ │ │ ├── homePage.html │ │ │ └── registerCustomerPage.html │ │ └── application.properties
The Home page
The homePage.html
is the landing page of our application. When we request the Root Context for the application (“/”) the Controller will redirect to this page:
<head> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" /> <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js"></script> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" /> </head> <body> <div class="col-5"> <div class="container"> <div class="card"> <div class="card-header bg-info text-center text-white"> <h3>Spring Boot MVC Example</h3> </div> <div class="card-body"> <form> <a th:href="@{/customer/register}" class="btn btn-success ">Add Customer <i class="fa fa-plus-square" aria-hidden="true"></i></a> <a th:href="@{/customer/getAllCustomers}" class="btn btn-primary">Show All Customers</a> </form> </div> </div> </div> </div> </body> </html>
Here is how to page will look when you request the customer endpoint (http://localhost:8080/customer
):
The Add Customer View
By Clicking on the “Add Customer” button, the navigation will continue in the registerCustomerPage.html
View which contains a Form to insert a new Customer:
<!DOCTYPE html> <html xmlns:th="https://www.thymeleaf.org/"> <head> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" /> <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js"></script> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" /> <style> .label-column { padding-right: 15px; } </style> </head> <body> <div class="col-5"> <div class="container"> <div class="card"> <div class="card-header bg-info text-center text-white"> <h3>Add Customer</h3> </div> <div class="card-body"> <form th:action="@{/customer/save}" method="POST" id="invoiceForm"> <div class="row"> <div class="col-2 label-column"> <label for="name"><b>NAME</b></label> </div> <div class="col-4"> <input type="text" name="name" id="name" class="form-control" /> </div> </div> <br /> <div class="row"> <div class="col-2 label-column"> <label for="surname"><b>SURNAME</b></label> </div> <div class="col-4"> <input type="text" name="surname" id="surname" class="form-control" /> </div> </div><br /> <div class="row"> <div class="col-2 label-column"> <label for="email"><b>EMAIL</b></label> </div> <div class="col-4"> <input type="text" name="email" id="email" class="form-control" /> </div> </div> <br /> <button type="submit" class="btn btn-success">Save Customer <i class="fa fa-plus-square" aria-hidden="true"></i></button> <a th:href="@{/customer/getAllCustomers}" class="btn btn-primary">Show All Customers</a> </form> </div> <div th:if="${message!=null}" class="card-footer bg-white text-info"> <span th:text="${message}"></span> </div> </div> </div> </div> </body> </html>
Here is our Add Customer View in Action:
The List Customer View
To list the Customers in a tabular style we can use the following View (allCustomersPage.html
) which will loop through the Customer. Within it, the th:each
loop is a Thymeleaf attribute used to iterate over a collection of objects and display them in the HTML template. In this specific context, it’s used to iterate over a list of Customer
objects and render each one as a row in an HTML table.
<html xmlns:th="https://www.thymeleaf.org/"> <head> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" /> <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js"></script> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" /> </head> <body> <div class="col-5"> <div class="container"> <div class="card"> <div class="card-header bg-info text-center text-white"> <h3>Customer List</h3> </div> <div class="card-body"> <table class="table table-hover"> <tr class="bg-dark text-white"> <th>ID</th> <th>Name</th> <th>Surname</th> <th>Email</th> <th>Edit/Delete</th> </tr> <tr th:each="ob:${list}"> <td th:text=${ob.id}></td> <td th:text=${ob.name}></td> <td th:text=${ob.surname}></td> <td th:text=${ob.email}></td> <td><a th:href="@{/customer/delete(id=${ob.id})}" class="btn btn-danger">DELETE <i class="fa fa-trash-o" aria-hidden="true"></i></a> | <a th:href="@{/customer/edit(id=${ob.id})}" class="btn btn-warning">EDIT <i class="fa fa-pencil-square-o" aria-hidden="true"></i></a> </td> </tr> </table> <a th:href="@{/customer/register}" class="btn btn-success ">Add Customer <i class="fa fa-plus-square" aria-hidden="true"></i></a> <a th:href="@{/customer/}" class="btn btn-primary">Go to Home</a> </div> <div class="card-footer bg-white text-success" th:if="${message!=null}"> <span th:text="${message}"></span> </div> </div> </div> </div> </body> </html>
Here is the Customer List View in Action:
The Edit Customer View
For the sake of completeness, we will also include the editCustomer.html
page which allows editing an existing Customer:
<html xmlns:th="https://www.thymeleaf.org/"> <head> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" /> <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js"></script> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" /> </head> <body> <div class="col-5"> <div class="container"> <div class="card"> <div class="card-header bg-primary text-white text-center"> <h3>Edit Customer</h3> </div> <div class="card-body"> <form th:action="@{/customer/update}" method="POST" id="invoiceForm" th:object="${customer}"> <div class="row"> <div class="col-2"> <label for="id">ID</label> </div> <div class="col-4"> <input type="text" th:field="*{id}" class="form-control" readonly /> </div> </div><br /> <div class="row"> <div class="col-2"> <label for="name">NAME</label> </div> <div class="col-4"> <input type="text" th:field="*{name}" class="form-control" /> </div> </div><br /> <div class="row"> <div class="col-2"> <label for="location">SURNAME</label> </div> <div class="col-4"> <input type="text" th:field="*{surname}" class="form-control" /> </div> </div><br /> <div class="row"> <div class="col-2"> <label for="amount">EMAIL</label> </div> <div class="col-4"> <input type="text" th:field="*{email}" class="form-control" /> </div> </div> <br /> <button type="submit" class="btn btn-success">Update <i class="fa fa-wrench" aria-hidden="true"></i></button> <a th:href="@{/customer/getAllCustomers}" class="btn btn-primary">Show All Customers</a> </form> </div> <div class="card-footer"></div> </div> </div> </div> </body> </html>
We are done. You can build the application and access it at http://localhost:8080/customer/
Source code: https://github.com/fmarchioni/masterspringboot/tree/master/front-ends/spring-mvc
Conclusion
This tutorial covered the basics of building a Spring Boot 3 MVC application with Thymeleaf for the view layer. You learned how to:
- Create a
Customer
entity. - Implement the repository and service layers.
- Build a controller to handle CRUD operations.
- Design views using Thymeleaf to interact with the customer data.
You can extend this project by adding more features like validation, error handling, and integrating with a persistent database like MySQL or PostgreSQL.
Found the article helpful? if so please follow us on Socials