Spring Boot and Thymeleaf for Modern Web Apps

Spring Boot and Thymeleaf make a fantastic duo for developing web applications. If you’re looking for a modern, lightweight, and developer-friendly way to build dynamic web pages with Java, this combination is hard to beat. Thymeleaf provides a clean and natural way to integrate templates with backend logic, while Spring Boot simplifies application setup and configuration.

What is Thymeleaf?

Thymeleaf is a Java-based templating engine primarily used for web applications. Unlike other templating engines like JSP (JavaServer Pages), Thymeleaf is designed to work seamlessly with HTML, making it easier for front-end developers and back-end developers to collaborate.

Some of the key features of Thymeleaf include:

  • Natural templating: Thymeleaf templates are valid HTML files that can be opened in a browser without requiring a running server.
  • Seamless integration with Spring Boot: Thymeleaf works effortlessly to render dynamic content.
  • Expression Language (EL): Allows dynamic data binding using expressions like ${user.name}.
  • Support for layouts and fragments: Helps in creating reusable UI components.
  • Security and internationalization support: Includes built-in features for handling security concerns and localization.

Why Use Thymeleaf with Spring Boot?

Spring Boot removes the complexity of configuring Spring-based applications, while Thymeleaf simplifies the process of rendering views. Together, they create an efficient, maintainable, and productive development environment.

Here’s why developers love using Thymeleaf with Spring Boot:

  1. Easy Setup — Spring Boot provides built-in support for Thymeleaf, making it easy to start without writing complex configurations.
  2. Spring MVC Integration — Thymeleaf works smoothly with Spring MVC, allowing easy data binding, form handling, and model updates.
  3. Readable and Maintainable Code — Thymeleaf templates are pure HTML files with a few additional attributes, and they are easier to read and maintain than JSP or other template-based approaches.
  4. Automatic Reloading — Spring Boot allows Thymeleaf templates to be reloaded automatically during development, improving productivity.
  5. Rich Features — Features like conditional rendering, loops, and inline expressions allow developers to build dynamic UI components effortlessly.

Setting Up a Spring Boot Project with Thymeleaf

Step 1: Create a Spring Boot Project

If you haven’t already set up a Spring Boot project, you can create one using Spring Initializr.

1. Go to Spring Initializr.

2. Choose the following settings:

  • Project: Maven
  • Language: Java
  • Spring Boot Version: The latest stable version
  • Dependencies: Select Spring Web and Thymeleaf

3. Click Generate to download the project, then extract and open it in your preferred IDE (e.g., IntelliJ IDEA, Eclipse, or VS Code).

Step 2: Configure Thymeleaf in Spring Boot

Spring Boot automatically configures Thymeleaf when the dependency is included in the project. The default settings assume that templates are stored in the src/main/resources/templates directory.

To confirm, check your application.properties file in src/main/resources:

spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.cache=false

Setting spring.thymeleaf.cache=false allows templates to be reloaded without restarting the application, which is helpful during development.

Step 3: Create a Simple Thymeleaf Template

Let’s create a basic HTML file for our Thymeleaf template.

Inside src/main/resources/templates/, create a new file called home.html and add the following content:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Spring Boot Thymeleaf Example</title>
</head>
<body>
    <h1>Welcome to Spring Boot with Thymeleaf!</h1>
    <p th:text="'Hello, ' + ${name} + '!'"></p>
</body>
</html>

Here, we use th:text to dynamically display the value of the name variable.

Step 4: Create a Spring Boot Controller

Let’s create a simple Spring Boot controller to pass data to our Thymeleaf template.

In src/main/java/com/example/demo/controller/, create a file named HomeController.java:

package com.example.demo.controller;

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("name", "Spring Boot Developer");
        return "home"; // Refers to home.html in the templates folder
    }
}

This controller:

  • Maps the root URL (/) to the home.html template.
  • Adds a dynamic value (name) to the model, which is then displayed in the HTML.

Step 5: Run the Spring Boot Application

With everything set up, run your Spring Boot application.

If you’re using Maven, run the following command in the terminal:

mvn spring-boot:run

Once the application starts, open your browser and go to http://localhost:8080/. You should see the welcome message along with the dynamically rendered name.

Home

Advanced Thymeleaf Features

1. Conditional Statements

Thymeleaf supports conditional statements using th:if and th:unless:

<p th:if="${age >= 18}">You are an adult.</p>
<p th:unless="${age >= 18}">You are a minor.</p>

2. Iterating Over Lists

Developers can loop through collections using tags. th:each

<ul>
    <li th:each="user : ${users}" th:text="${user.name}"></li>
</ul>

3. Using Layouts and Fragments

To create reusable components, Thymeleaf provides the th:replace and th:insert attributes.

Header Fragment (header.html)

<div th:fragment="header">
    <h1>My Website</h1>
</div>

Main Template (home.html)

<html>
<body>
    <div th:replace="fragments/header :: header"></div>
    <p>Welcome to my website!</p>
</body>
</html>

Thymeleaf integrates with HTMX and Tailwind CSS

Features:

  • Uses Thymeleaf for server-side rendering.
  • Uses HTMX for AJAX-based dynamic updates.
  • Uses Tailwind CSS for styling.

Add Dependencies

Add the following dependencies to pom.xml:

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Thymeleaf -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>

    <!-- HTMX -->
    <dependency>
        <groupId>org.webjars.npm</groupId>
        <artifactId>htmx.org</artifactId>
        <version>1.9.6</version>
    </dependency>
</dependencies>

Example: Submit form.

Create a Controller

Create a simple controller in HomeController.java:

package com.example.demo.controller;

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

@Controller
public class HomeController {

    @GetMapping("/")
    public String home(Model model) {
        model.addAttribute("message", "Welcome to Thymeleaf + HTMX + Tailwind!");
        return "index";
    }

    @PostMapping("/update")
    public String updateMessage(@RequestParam String name, Model model) {
        model.addAttribute("message", "Hello, " + name + "!");
        return "fragments/message :: messageContent";
    }
}

Create Thymeleaf Templates

src/main/resources/templates/index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Thymeleaf + HTMX + Tailwind</title>
    <script src="/webjars/htmx.org/1.9.6/dist/htmx.min.js"></script>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="flex items-center justify-center min-h-screen bg-gray-100">
    <div class="p-6 bg-white shadow-lg rounded-lg">
        <h1 class="text-2xl font-bold text-blue-600" id="message" th:text="${message}"></h1>
        
        <form hx-post="/update" hx-target="#message" hx-swap="outerHTML" class="mt-4">
            <input type="text" name="name" placeholder="Enter your name"
                   class="p-2 border rounded w-full">
            <button type="submit" class="mt-2 px-4 py-2 bg-blue-500 text-white rounded">
                Submit
            </button>
        </form>
    </div>
</body>
</html>
src/main/resources/templates/fragments/message.html
<div id="message" class="text-2xl font-bold text-blue-600" th:text="${message}" th:fragment="messageContent"></div>

Run the Application

mvn spring-boot:run

Visit: http://localhost:8080/

Home

Input “John” and submit.

Input

Explain attribute

  • hx-post="/update" sends a POST request to the /update URL when triggered by an event.
  • hx-target="#message" Specifies the element with ID message to update with the response content.
  • hx-swap="outerHTML" Replace the entire target element’s HTML with the response content.

Example: Analytics Dashboard

Create a Controller

Create a simple controller in DashboardController.java:

import org.springframework.web.bind.annotation.*;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import java.util.*;
import java.time.LocalDateTime;
import lombok.Data;

@Controller
public class DashboardController {

    private Random random = new Random();
    private List<String> userNames = Arrays.asList("Alice Smith", "Bob Johnson", "Carol Williams", "David Brown", "Eve Davis");
    private List<String> actions = Arrays.asList("Logged in", "Updated profile", "Made purchase", "Added review", "Shared content");

    @GetMapping("/dashboard")
    public String dashboard(Model model) {
        return "dashboard";
    }

    // Stats endpoints return fragments that will replace the card contents
    @GetMapping("/api/stats/users")
    public String getTotalUsers(Model model) {
        // Simulate growing user base
        int baseUsers = 10000;
        int variation = random.nextInt(500);
        model.addAttribute("totalUsers", baseUsers + variation);
        return "fragments/stats :: userCard";
    }

    @GetMapping("/api/stats/sessions")
    public String getActiveSessions(Model model) {
        // Simulate active sessions with random fluctuation
        int baseSessions = 250;
        int variation = random.nextInt(100) - 50; // Can go up or down
        model.addAttribute("activeSessions", Math.max(0, baseSessions + variation));
        return "fragments/stats :: sessionCard";
    }

    @GetMapping("/api/stats/errors")
    public String getErrorRate(Model model) {
        // Simulate error rate percentage
        double baseErrorRate = 0.5;
        double variation = random.nextDouble() * 0.3;
        model.addAttribute("errorRate", String.format("%.2f", baseErrorRate + variation));
        return "fragments/stats :: errorCard";
    }

    @GetMapping("/api/activity")
    public String getRecentActivity(Model model) {
        List<ActivityEntry> activities = generateRecentActivities();
        model.addAttribute("recentActivity", activities);
        return "fragments/activity :: activityTable";
    }

    private List<ActivityEntry> generateRecentActivities() {
        List<ActivityEntry> activities = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            ActivityEntry entry = new ActivityEntry();
            entry.setUserName(userNames.get(random.nextInt(userNames.size())));
            entry.setAction(actions.get(random.nextInt(actions.size())));
            entry.setTimestamp(LocalDateTime.now().minusMinutes(random.nextInt(60)));
            entry.setUserAvatar("https://ui-avatars.com/api/?name=" + entry.getUserName().replace(" ", "+"));
            activities.add(entry);
        }
        return activities;
    }

    @Data
    public static class ActivityEntry {
        private String userName;
        private String userAvatar;
        private String action;
        private LocalDateTime timestamp;
    }
}

Create Thymeleaf Templates

src/main/resources/templates/dashboard.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Analytics Dashboard</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/htmx/1.9.6/htmx.min.js"></script>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100">
<div class="min-h-screen">
    <!-- Navbar -->
    <nav class="bg-white shadow-lg">
        <div class="max-w-7xl mx-auto px-4">
            <div class="flex justify-between h-16">
                <div class="flex items-center">
                    <span class="text-xl font-semibold">Analytics Dashboard</span>
                </div>
                <div class="flex items-center">
                        <span th:text="${#dates.format(#dates.createNow(), 'MMM dd, yyyy')}"
                              class="text-gray-600"></span>
                </div>
            </div>
        </div>
    </nav>

    <!-- Main Content -->
    <main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
        <!-- Stats Grid -->
        <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
            <!-- Total Users Card -->
            <div class="bg-white overflow-hidden shadow rounded-lg"
                 hx-get="/api/stats/users"
                 hx-trigger="load, every 30s">
                <div class="p-5">
                    <div class="flex items-center">
                        <div class="flex-shrink-0 bg-indigo-500 rounded-md p-3">
                            <svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                                      d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
                            </svg>
                        </div>
                        <div class="ml-5 w-0 flex-1">
                            <dl>
                                <dt class="text-sm font-medium text-gray-500 truncate">
                                    Total Users
                                </dt>
                                <dd class="flex items-baseline">
                                    <div class="text-2xl font-semibold text-gray-900" th:text="${totalUsers}">
                                        0
                                    </div>
                                </dd>
                            </dl>
                        </div>
                    </div>
                </div>
            </div>

            <!-- Active Sessions Card -->
            <div class="bg-white overflow-hidden shadow rounded-lg"
                 hx-get="/api/stats/sessions"
                 hx-trigger="load, every 10s">
                <div class="p-5">
                    <div class="flex items-center">
                        <div class="flex-shrink-0 bg-green-500 rounded-md p-3">
                            <svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                                      d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
                            </svg>
                        </div>
                        <div class="ml-5 w-0 flex-1">
                            <dl>
                                <dt class="text-sm font-medium text-gray-500 truncate">
                                    Active Sessions
                                </dt>
                                <dd class="flex items-baseline">
                                    <div class="text-2xl font-semibold text-gray-900" th:text="${activeSessions}">
                                        0
                                    </div>
                                </dd>
                            </dl>
                        </div>
                    </div>
                </div>
            </div>

            <!-- Error Rate Card -->
            <div class="bg-white overflow-hidden shadow rounded-lg"
                 hx-get="/api/stats/errors"
                 hx-trigger="load, every 15s">
                <div class="p-5">
                    <div class="flex items-center">
                        <div class="flex-shrink-0 bg-red-500 rounded-md p-3">
                            <svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                                      d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
                            </svg>
                        </div>
                        <div class="ml-5 w-0 flex-1">
                            <dl>
                                <dt class="text-sm font-medium text-gray-500 truncate">
                                    Error Rate
                                </dt>
                                <dd class="flex items-baseline">
                                    <div class="text-2xl font-semibold text-gray-900">
                                        <span th:text="${errorRate}">0</span>%
                                    </div>
                                </dd>
                            </dl>
                        </div>
                    </div>
                </div>
            </div>
        </div>

        <!-- Recent Activity Table -->
        <div class="bg-white shadow rounded-lg"
             hx-get="/api/activity"
             hx-trigger="load, every 20s">
            <div class="px-4 py-5 sm:p-6">
                <h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">
                    Recent Activity
                </h3>
                <div class="flex flex-col">
                    <div class="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
                        <div class="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
                            <div class="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
                                <table class="min-w-full divide-y divide-gray-200">
                                    <thead class="bg-gray-50">
                                    <tr>
                                        <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                                            User
                                        </th>
                                        <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                                            Action
                                        </th>
                                        <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                                            Time
                                        </th>
                                    </tr>
                                    </thead>
                                    <tbody class="bg-white divide-y divide-gray-200">
                                    <tr th:each="activity : ${recentActivity}">
                                        <td class="px-6 py-4 whitespace-nowrap">
                                            <div class="flex items-center">
                                                <div class="h-10 w-10 flex-shrink-0">
                                                    <img class="h-10 w-10 rounded-full"
                                                         th:src="${activity.userAvatar}"
                                                         alt="">
                                                </div>
                                                <div class="ml-4">
                                                    <div class="text-sm font-medium text-gray-900"
                                                         th:text="${activity.userName}">
                                                    </div>
                                                </div>
                                            </div>
                                        </td>
                                        <td class="px-6 py-4 whitespace-nowrap">
                                                    <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"
                                                          th:text="${activity.action}">
                                                    </span>
                                        </td>
                                        <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"
                                            th:text="${#dates.format(activity.timestamp, 'HH:mm:ss')}">
                                        </td>
                                    </tr>
                                    </tbody>
                                </table>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </main>
</div>
</body>
</html>
src/main/resources/templates/fragments/activity.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<div th:fragment="activityTable">
    <div class="flex flex-col">
        <div class="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
            <div class="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
                <div class="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
                    <table class="min-w-full divide-y divide-gray-200">
                        <thead class="bg-gray-50">
                        <tr>
                            <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                                User
                            </th>
                            <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                                Action
                            </th>
                            <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                                Time
                            </th>
                        </tr>
                        </thead>
                        <tbody class="bg-white divide-y divide-gray-200">
                        <tr th:each="activity : ${recentActivity}">
                            <td class="px-6 py-4 whitespace-nowrap">
                                <div class="flex items-center">
                                    <div class="h-10 w-10 flex-shrink-0">
                                        <img class="h-10 w-10 rounded-full"
                                             th:src="${activity.userAvatar}"
                                             alt="">
                                    </div>
                                    <div class="ml-4">
                                        <div class="text-sm font-medium text-gray-900"
                                             th:text="${activity.userName}">
                                        </div>
                                    </div>
                                </div>
                            </td>
                            <td class="px-6 py-4 whitespace-nowrap">
                                    <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"
                                          th:text="${activity.action}">
                                    </span>
                            </td>
                            <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"
                                th:text="${#temporals.format(activity.timestamp, 'HH:mm:ss')}">
                            </td>
                        </tr>
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
</div>
</html>
src/main/resources/templates/fragments/stats.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<!-- User Card Fragment -->
<div th:fragment="userCard">
    <div class="flex items-center">
        <div class="flex-shrink-0 bg-indigo-500 rounded-md p-3">
            <svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                      d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
            </svg>
        </div>
        <div class="ml-5 w-0 flex-1">
            <dl>
                <dt class="text-sm font-medium text-gray-500 truncate">
                    Total Users
                </dt>
                <dd class="flex items-baseline">
                    <div class="text-2xl font-semibold text-gray-900" th:text="${totalUsers}">
                        0
                    </div>
                </dd>
            </dl>
        </div>
    </div>
</div>

Run the Application

mvn spring-boot:run

Visit: http://localhost:8080/dashboard

Dashboard

Explain tag

  • hx-trigger="load, every 30s" to trigger an HTMX request when the element loads and every
  • hx-get="/api/stats/users" triggers a GET request to the specified URL (/api/stats/users).

Alternative UI Components

1. Thymeleaf + Bootstrap:

  • Bootstrap is a popular CSS framework that works seamlessly with Thymeleaf to create responsive and visually appealing components.
  • You can integrate Bootstrap into Thymeleaf templates by adding the CSS and JS files, either from a CDN or your static resources, and using Bootstrap classes directly in your HTML.

2. Thymeleaf + UI Libraries:

  • Thymeleaf + Materialize CSS: Materialize is based on Google’s Material Design, providing a modern, user-friendly UI.
  • Thymeleaf + Bulma: Bulma is a modern CSS framework that is flexible, simple, and responsive.
  • Thymeleaf + Tailwind CSS: A utility-first CSS framework that lets you easily design custom components.

3. Thymeleaf Layout Dialect:

  • Use Thymeleaf Layout Dialect to handle reusable UI components like headers, footers, and navigation bars, which helps maintain a consistent page layout.

Conclusion

Spring Boot and Thymeleaf provide a powerful yet simple way to build modern web applications with Java. With its intuitive syntax, seamless integration with Spring, and support for dynamic content, Thymeleaf is an excellent choice for developers who want to create maintainable and user-friendly web pages.

This article was originally published on Medium.

Leave a Comment

Your email address will not be published. Required fields are marked *