Lab: Spring MVC with Thymeleaf


This lab will guide you through creating a simple web application using Spring Boot, Spring MVC, and Thymeleaf. By the end of this exercise, you will understand how the Model-View-Controller pattern works in Spring applications and how Thymeleaf integrates with Spring to render dynamic web pages.

Prerequisites

  • Basic Java programming knowledge
  • Familiarity with HTML and CSS
  • Maven or Gradle build tools installed
  • Java JDK 11 or higher installed
  • An IDE (IntelliJ IDEA, Eclipse, or VS Code)

Lab Objectives

After completing this lab, you will be able to:

  1. Set up a Spring Boot project with Spring MVC and Thymeleaf
  2. Understand the Model-View-Controller architecture
  3. Create controllers to handle HTTP requests
  4. Use the Model to pass data to views
  5. Create Thymeleaf templates to display dynamic content
  6. Process form submissions
  7. Implement basic validation

Step 1: Project Setup

Let’s begin by creating a Spring Boot project:

  1. Go to Spring Initializr
  2. Configure the project:
  • Project: Maven
  • Language: Java
  • Spring Boot: 3.0.x (or latest stable)
  • Project Metadata:
    • Group: com.example
    • Artifact: spring-thymeleaf-lab
    • Name: spring-thymeleaf-lab
    • Description: Spring MVC with Thymeleaf Lab
    • Package name: com.example.spring-thymeleaf-lab
    • Packaging: Jar
    • Java: 17 (or your installed version)
  1. Add Dependencies:
  • Spring Web
  • Spring Boot DevTools
  • Thymeleaf
  • Validation
  1. Click “Generate” to download the project ZIP file
  2. Extract and open in your preferred IDE

Step 2: Understanding the Project Structure

Take a moment to explore the project structure:

src/main/java/com/example/springthymeleaflab/
├── SpringThymeleafLabApplication.java (main application class)
src/main/resources/
├── application.properties (configuration file)
├── static/ (for static resources like CSS, JS, images)
└── templates/ (for Thymeleaf templates)

This structure follows Spring Boot conventions. The SpringThymeleafLabApplication.java contains the main method and is annotated with @SpringBootApplication.

Step 3: Create a Controller

Let’s create our first controller:

  1. Create a new package controllers under com.example.springthymeleaflab
  2. Create a new class HomeController.java in this package:
package com.example.springthymeleaflab.controllers;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {

    @GetMapping("/")
    public String home(Model model) {
        model.addAttribute("message", "Welcome to Spring MVC with Thymeleaf!");
        return "home";
    }
}

Key points to explain:

  • @Controller marks this class as a Spring MVC controller
  • @GetMapping("/") maps HTTP GET requests to “/” to this method
  • Model is used to pass data to the view
  • return "home" tells Spring to use a template named “home.html”

Step 4: Create a Thymeleaf Template

  1. Create a file home.html in src/main/resources/templates/:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title>Spring MVC with Thymeleaf</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        margin: 40px;
        line-height: 1.6;
      }
      .container {
        max-width: 800px;
        margin: 0 auto;
        padding: 20px;
        border: 1px solid #ddd;
        border-radius: 5px;
      }
      h1 {
        color: #3498db;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h1>Home Page</h1>
      <p th:text="${message}">Default message (this will be replaced)</p>
    </div>
  </body>
</html>

Key points to explain:

  • xmlns:th="http://www.thymeleaf.org" enables Thymeleaf namespace
  • th:text="${message}" is a Thymeleaf expression that will be replaced with the value of the “message” attribute from the Model
  • The default text “Default message” is only displayed when viewing the HTML directly without Thymeleaf processing

Step 5: Run the Application

  1. Run the application (in your IDE or with mvn spring-boot:run)
  2. Open a browser and go to http://localhost:8080
  3. You should see the welcome message from the controller displayed on the page

Step 6: Create a Model Class

Now, let’s create a simple domain model:

  1. Create a new package models under com.example.springthymeleaflab
  2. Create a Student.java class:
package com.example.springthymeleaflab.models;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public class Student {

    @NotBlank(message = "Name is required")
    @Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
    private String name;

    @NotBlank(message = "Email is required")
    @Email(message = "Please provide a valid email address")
    private String email;

    @NotBlank(message = "Major is required")
    private String major;

    // Default constructor
    public Student() {
    }

    // Constructor with fields
    public Student(String name, String email, String major) {
        this.name = name;
        this.email = email;
        this.major = major;
    }

    // Getters and Setters
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getMajor() {
        return major;
    }

    public void setMajor(String major) {
        this.major = major;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", email='" + email + '\'' +
                ", major='" + major + '\'' +
                '}';
    }
}

Key points to explain:

  • We’ve created a simple Student class with validation annotations
  • @NotBlank, @Size, and @Email are validation annotations from the Jakarta Validation API
  • Getters and setters are required for data binding in Spring MVC

Step 7: Create a Student Controller

Create a StudentController.java in the controllers package:

package com.example.springthymeleaflab.controllers;

import com.example.springthymeleaflab.models.Student;
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.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import jakarta.validation.Valid;
import java.util.ArrayList;
import java.util.List;

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

    // In-memory storage for students (in a real app, this would be a database)
    private List<Student> students = new ArrayList<>();

    @GetMapping("/list")
    public String listStudents(Model model) {
        model.addAttribute("students", students);
        return "students/list";
    }

    @GetMapping("/register")
    public String showRegistrationForm(Model model) {
        model.addAttribute("student", new Student());
        return "students/register";
    }

    @PostMapping("/register")
    public String registerStudent(@Valid @ModelAttribute("student") Student student,
                                 BindingResult result, Model model) {
        if (result.hasErrors()) {
            return "students/register";
        }

        students.add(student);
        return "redirect:/students/list";
    }
}

Key points to explain:

  • @RequestMapping("/students") is a base path for all methods in this controller
  • @GetMapping and @PostMapping map HTTP methods to controller methods
  • @Valid triggers validation of the Student object
  • BindingResult holds validation results
  • We’re using a simple ArrayList to store students (in a real app, you’d use a database)
  • redirect: prefix tells Spring to send a redirect response

Step 8: Create Thymeleaf Templates for Student Operations

  1. Create a directory students under src/main/resources/templates/
  2. Create list.html in this directory:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title>Student List</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        margin: 40px;
        line-height: 1.6;
      }
      .container {
        max-width: 800px;
        margin: 0 auto;
        padding: 20px;
        border: 1px solid #ddd;
        border-radius: 5px;
      }
      h1 {
        color: #3498db;
      }
      table {
        width: 100%;
        border-collapse: collapse;
        margin-top: 20px;
      }
      th,
      td {
        padding: 12px;
        text-align: left;
        border-bottom: 1px solid #ddd;
      }
      th {
        background-color: #f2f2f2;
      }
      .btn {
        display: inline-block;
        padding: 8px 16px;
        background-color: #3498db;
        color: white;
        text-decoration: none;
        border-radius: 4px;
      }
      .empty-message {
        font-style: italic;
        color: #777;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h1>Student List</h1>

      <a th:href="@{/students/register}" class="btn">Register New Student</a>

      <div th:if="${students.empty}" class="empty-message">
        <p>No students registered yet.</p>
      </div>

      <table th:unless="${students.empty}">
        <thead>
          <tr>
            <th>Name</th>
            <th>Email</th>
            <th>Major</th>
          </tr>
        </thead>
        <tbody>
          <tr th:each="student : ${students}">
            <td th:text="${student.name}">John Doe</td>
            <td th:text="${student.email}">[email protected]</td>
            <td th:text="${student.major}">Computer Science</td>
          </tr>
        </tbody>
      </table>

      <p>
        <a th:href="@{/}" class="btn">Back to Home</a>
      </p>
    </div>
  </body>
</html>
  1. Create register.html in the same directory:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title>Register Student</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        margin: 40px;
        line-height: 1.6;
      }
      .container {
        max-width: 600px;
        margin: 0 auto;
        padding: 20px;
        border: 1px solid #ddd;
        border-radius: 5px;
      }
      h1 {
        color: #3498db;
      }
      .form-group {
        margin-bottom: 15px;
      }
      label {
        display: block;
        margin-bottom: 5px;
        font-weight: bold;
      }
      input,
      select {
        width: 100%;
        padding: 8px;
        border: 1px solid #ddd;
        border-radius: 4px;
        box-sizing: border-box;
      }
      .error {
        color: red;
        font-size: 0.9em;
        margin-top: 5px;
      }
      .btn {
        display: inline-block;
        padding: 10px 20px;
        background-color: #3498db;
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
      }
      .btn-cancel {
        background-color: #95a5a6;
        margin-left: 10px;
        text-decoration: none;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h1>Register Student</h1>

      <form
        th:action="@{/students/register}"
        th:object="${student}"
        method="post"
      >
        <div class="form-group">
          <label for="name">Name:</label>
          <input type="text" th:field="*{name}" id="name" />
          <div
            th:if="${#fields.hasErrors('name')}"
            class="error"
            th:errors="*{name}"
          >
            Name Error
          </div>
        </div>

        <div class="form-group">
          <label for="email">Email:</label>
          <input type="email" th:field="*{email}" id="email" />
          <div
            th:if="${#fields.hasErrors('email')}"
            class="error"
            th:errors="*{email}"
          >
            Email Error
          </div>
        </div>

        <div class="form-group">
          <label for="major">Major:</label>
          <select th:field="*{major}" id="major">
            <option value="">Select a major</option>
            <option value="Computer Science">Computer Science</option>
            <option value="Information Technology">
              Information Technology
            </option>
            <option value="Data Science">Data Science</option>
            <option value="Software Engineering">Software Engineering</option>
            <option value="Cybersecurity">Cybersecurity</option>
          </select>
          <div
            th:if="${#fields.hasErrors('major')}"
            class="error"
            th:errors="*{major}"
          >
            Major Error
          </div>
        </div>

        <div class="form-group">
          <button type="submit" class="btn">Register</button>
          <a th:href="@{/students/list}" class="btn btn-cancel">Cancel</a>
        </div>
      </form>
    </div>
  </body>
</html>

Key points to explain:

  • th:each is used for iteration (like a for-each loop)
  • th:if and th:unless are conditional statements
  • th:field binds form elements to model attributes
  • th:errors displays validation error messages
  • #fields.hasErrors() checks if there are validation errors
  • @{...} syntax creates URL links with proper context paths

Step 9: Update the Home Page

Update the home.html file to include links to the student pages:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title>Spring MVC with Thymeleaf</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        margin: 40px;
        line-height: 1.6;
      }
      .container {
        max-width: 800px;
        margin: 0 auto;
        padding: 20px;
        border: 1px solid #ddd;
        border-radius: 5px;
      }
      h1 {
        color: #3498db;
      }
      .btn {
        display: inline-block;
        margin-top: 20px;
        padding: 10px 20px;
        background-color: #3498db;
        color: white;
        text-decoration: none;
        border-radius: 4px;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h1>Home Page</h1>
      <p th:text="${message}">Default message</p>

      <h2>Spring MVC with Thymeleaf Demo</h2>
      <p>
        This application demonstrates the basic concepts of Spring MVC and
        Thymeleaf.
      </p>

      <div>
        <a th:href="@{/students/list}" class="btn">View Students</a>
        <a th:href="@{/students/register}" class="btn">Register Student</a>
      </div>
    </div>
  </body>
</html>

Step 10: Run and Test the Complete Application

  1. Run the application
  2. Open a browser and go to http://localhost:8080
  3. Navigate through the app:
  • Click “Register Student”
  • Fill in the form (try both valid and invalid data)
  • View the student list

Conceptual Explanation

During the lab, be sure to understand these key concepts:

  1. Spring MVC Architecture:
  • Model: Java objects that hold application data (like our Student class)
  • View: The Thymeleaf templates that render the UI
  • Controller: Java classes that handle HTTP requests and responses
  1. Data Flow in Spring MVC:
  • Browser sends a request to a specific URL
  • DispatcherServlet (Spring’s front controller) processes the request
  • The request is mapped to the appropriate controller method
  • The controller processes the request and puts data in the Model
  • The controller returns a view name
  • Thymeleaf renders the view with data from the Model
  • The response is sent back to the browser
  1. Thymeleaf Key Features:
  • Natural templating: HTML files remain valid even when not processed by Thymeleaf
  • Expression syntax: ${...} for variables, *{...} for object properties
  • Link URL creation: @{...}
  • Iteration: th:each
  • Conditionals: th:if, th:unless
  • Form binding: th:field, th:object

Homework Assignments

To reinforce learning, assign these extensions:

  1. Add the ability to edit existing students
  2. Add a delete functionality for students
  3. Add a search feature to find students by name or major
  4. Create a dashboard that shows statistics (number of students per major)
  5. Improve the validation with custom error messages

Conclusion

This lab provides a comprehensive introduction to Spring MVC with Thymeleaf. You will gain an understanding of how the MVC pattern works in Spring applications and how Thymeleaf integrates with Spring to create dynamic web pages.


By Wahid Hamdi