Spring Boot
Thymeleaf Integration
Thymeleaf is the recommended server-side template engine for Spring Boot. It processes HTML templates on the server, binding model data to the markup using th:* attributes. Templates are valid HTML files that open correctly in a browser without a running server ā a property Thymeleaf calls Natural Templating. Spring Boot auto-configures everything with a single starter dependency.
Setup and Auto-Configuration
Adding spring-boot-starter-thymeleaf to the classpath triggers ThymeleafAutoConfiguration. It registers a ThymeleafViewResolver, a SpringTemplateEngine, and a ClassLoaderTemplateResolver ā all with sensible defaults. No Java configuration is required.
XML
<!-- pom.xml: -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
# āā application.yml ā default values (only override when needed): āāāāā
spring:
thymeleaf:
prefix: classpath:/templates/ # template root
suffix: .html # file extension
mode: HTML # template mode: HTML, XML, TEXT, JAVASCRIPT, CSS
encoding: UTF-8
cache: false # disable in dev; set true in prod
check-template-location: true # fail fast if templates/ directory missing
check-template: true # fail fast if a specific template is missing
servlet:
content-type: text/html # response Content-Type
# āā Template file layout: āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
# src/main/resources/
# āāā templates/
# āāā index.html
# āāā layout/
# ā āāā base.html ā shared layout fragment
# āāā users/
# ā āāā list.html ā resolved by "users/list"
# ā āāā detail.html ā resolved by "users/detail"
# ā āāā form.html ā resolved by "users/form"
# āāā error/
# āāā 404.html
# āāā 500.htmlCore Thymeleaf Expressions
Thymeleaf uses five expression syntaxes inside th:* attributes. Understanding them is the foundation for all template work.
html
<!-- āā Variable expression ${...} ā access model attributes: āāāāāāāāā -->
<p th:text="${user.name}">Placeholder name</p>
<p th:text="${user.role.name()}">ROLE</p>
<!-- āā Selection expression *{...} ā access fields on th:object: āāāāāā -->
<form th:object="${userForm}">
<input th:field="*{name}" /> <!-- = th:field="${userForm.name}" -->
<input th:field="*{email}" />
</form>
<!-- āā Message expression #{...} ā internationalisation keys: āāāāāāāāāā -->
<h1 th:text="#{page.users.title}">Users</h1>
<p th:text="#{validation.required}">This field is required</p>
<!-- āā Link expression @{...} ā URL building: āāāāāāāāāāāāāāāāāāāāāāāāāā -->
<a th:href="@{/users}">All users</a>
<a th:href="@{/users/{id}(id=${user.id})}">View user</a>
<a th:href="@{/users(page=${page},size=20)}">Page</a> <!-- query params -->
<img th:src="@{/static/images/logo.png}" />
<!-- āā Fragment expression ~{...} ā include fragments: āāāāāāāāāāāāāāāā -->
<div th:insert="~{layout/base :: header}"></div>
<div th:replace="~{layout/base :: footer}"></div>
<!-- āā Literals and operations inside expressions: āāāāāāāāāāāāāāāāāāāāā -->
<p th:text="'Hello, ' + ${user.name} + '!'"></p>
<p th:text="|Hello, ${user.name}!|"></p> <!-- literal substitution -->
<p th:text="${user.age >= 18 ? 'Adult' : 'Minor'}"></p> <!-- ternary -->
<p th:text="${user.bio} ?: 'No bio provided'"></p> <!-- Elvis -->
<p th:text="${user.name?.toUpperCase()}"></p> <!-- safe navigation -->Iteration, Conditionals, and Attributes
th:each iterates over collections, th:if and th:unless control conditional rendering, and th:attr sets arbitrary HTML attributes. These three cover the vast majority of dynamic template logic.
html
<!-- āā th:each ā iterate over a collection: āāāāāāāāāāāāāāāāāāāāāāāāāāā -->
<tr th:each="user : ${users}">
<td th:text="${user.name}"></td>
<td th:text="${user.email}"></td>
</tr>
<!-- th:each provides an iteration status variable: -->
<tr th:each="user, stat : ${users}"
th:classappend="${stat.odd} ? 'odd' : 'even'">
<td th:text="${stat.index}"></td> <!-- 0-based index -->
<td th:text="${stat.count}"></td> <!-- 1-based count -->
<td th:text="${stat.size}"><td> <!-- total size -->
<td th:text="${stat.first}"><td> <!-- true on first iteration -->
<td th:text="${stat.last}"><td> <!-- true on last iteration -->
<td th:text="${user.name}"></td>
</tr>
<!-- āā th:if / th:unless ā conditional rendering: āāāāāāāāāāāāāāāāāāāāā -->
<p th:if="${users.empty}">No users found.</p>
<p th:unless="${users.empty}" th:text="${users.size()} + ' users found'"></p>
<!-- th:if on a container removes both the element and its children: -->
<div th:if="${currentUser.admin}">
<a href="/admin">Admin Panel</a>
</div>
<!-- āā th:switch / th:case: āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā -->
<span th:switch="${user.role}">
<span th:case="'ADMIN'" class="badge-red">Admin</span>
<span th:case="'USER'" class="badge-blue">User</span>
<span th:case="*" class="badge-gray">Unknown</span>
</span>
<!-- āā th:classappend ā add CSS class conditionally: āāāāāāāāāāāāāāāāāā -->
<tr th:classappend="${user.active} ? 'active' : 'inactive'">
<!-- āā th:attr ā set any HTML attribute: āāāāāāāāāāāāāāāāāāāāāāāāāāāāāā -->
<input th:attr="placeholder=#{form.name.placeholder},
maxlength=${maxLength}" />
<!-- āā th:attrappend / th:attrprepend: āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā -->
<input class="form-control" th:attrappend="class=${' is-invalid'}"/>
<!-- āā th:remove ā remove elements from the final output: āāāāāāāāāāāāā -->
<tr th:remove="all-but-first"> <!-- keep only the first row in static preview -->
<td>Sample data</tr>
</tr>Fragments and Layouts
Thymeleaf fragments define reusable template sections. th:fragment marks a fragment; th:insert and th:replace include it. The Thymeleaf Layout Dialect (a separate library) extends this with a full inheritance-based layout system.
html
<!-- āā Define fragments in a shared file (templates/layout/base.html): ā -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="head(title)">
<meta charset="UTF-8" />
<title th:text="${title} + ' | MyApp'">MyApp</title>
<link rel="stylesheet" th:href="@{/static/css/main.css}" />
</head>
<nav th:fragment="navbar">
<a th:href="@{/}">Home</a>
<a th:href="@{/users}">Users</a>
</nav>
<footer th:fragment="footer">
<p>Ā© 2024 MyApp</p>
</footer>
</html>
<!-- āā Include fragments in a page template: āāāāāāāāāāāāāāāāāāāāāāāāāā -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{layout/base :: head('Users')}"></head>
<body>
<!-- th:insert ā keeps the container element, inserts fragment inside: -->
<div th:insert="~{layout/base :: navbar}"></div>
<!-- th:replace ā replaces the container element with the fragment: -->
<nav th:replace="~{layout/base :: navbar}"></nav>
<main>
<h1>Users</h1>
<!-- page-specific content -->
</main>
<th:block th:replace="~{layout/base :: footer}"></th:block>
</body>
</html>
<!-- āā Fragment with parameters: āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā -->
<!-- Define: -->
<div th:fragment="userCard(user)">
<h3 th:text="${user.name}"></h3>
<p th:text="${user.email}"></p>
</div>
<!-- Use: -->
<div th:replace="~{fragments/user :: userCard(${selectedUser})}"></div>
<!-- āā Inline fragment (same file): āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā -->
<div th:replace="~{:: #my-fragment}"></div>
<div id="my-fragment">content</div>Utility Objects
Thymeleaf exposes utility objects in all template expressions under the # prefix. These provide formatting, date handling, string manipulation, and collection operations without needing model attributes.
html
<!-- āā #dates / #temporals ā date and time formatting: āāāāāāāāāāāāāāāā -->
<p th:text="${#temporals.format(user.createdAt, 'dd MMM yyyy')}"></p>
<p th:text="${#temporals.format(user.createdAt, 'dd/MM/yyyy HH:mm')}"></p>
<p th:text="${#dates.format(legacyDate, 'yyyy-MM-dd')}"></p>
<!-- āā #numbers ā number formatting: āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā -->
<p th:text="${#numbers.formatDecimal(price, 1, 2)}"></p> <!-- 1,234.56 -->
<p th:text="${#numbers.formatCurrency(amount, 'en', 'GBP')}"></p>
<!-- āā #strings ā string utilities: āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā -->
<p th:text="${#strings.toUpperCase(user.name)}"></p>
<p th:text="${#strings.abbreviate(user.bio, 100)}"></p>
<p th:if="${#strings.isEmpty(user.bio)}">No bio</p>
<p th:text="${#strings.replace(text, 'old', 'new')}"></p>
<!-- āā #lists / #sets ā collection utilities: āāāāāāāāāāāāāāāāāāāāāāāāā -->
<p th:text="${#lists.size(users)}"></p>
<p th:if="${#lists.isEmpty(users)}">Empty</p>
<!-- āā #fields ā form validation (see Form Validation entry): āāāāāāāāāā -->
<span th:if="${#fields.hasErrors('email')}"
th:errors="*{email}"></span>
<!-- āā #request / #session / #servletContext: āāāāāāāāāāāāāāāāāāāāāāāāāā -->
<p th:text="${#request.getParameter('q')}"></p>
<p th:text="${#session.getAttribute('userId')}"></p>
<!-- āā Inline expressions ā use Thymeleaf inside text nodes: āāāāāāāāāāā -->
<p>Hello, [[${user.name}]]!</p> <!-- escaped output -->
<script th:inline="javascript">
const userId = [[${user.id}]]; <!-- JS-safe output -->
const userName = [[${user.name}]];
</script>Spring Security Integration
The Thymeleaf Extras Spring Security library adds sec:* attributes for showing or hiding content based on authentication state and roles ā without writing controller logic for display conditions.
XML
<!-- pom.xml: -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
<!-- Template ā add the sec namespace: -->
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<!-- āā Show only when authenticated: āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā -->
<div sec:authorize="isAuthenticated()">
<p>Welcome back!</p>
</div>
<!-- āā Show only when NOT authenticated: āāāāāāāāāāāāāāāāāāāāāāāāāāāāāā -->
<div sec:authorize="!isAuthenticated()">
<a th:href="@{/login}">Login</a>
<a th:href="@{/register}">Register</a>
</div>
<!-- āā Show only for a specific role: āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā -->
<div sec:authorize="hasRole('ADMIN')">
<a th:href="@{/admin}">Admin Panel</a>
</div>
<!-- āā Show for any of multiple roles: āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā -->
<div sec:authorize="hasAnyRole('ADMIN', 'MODERATOR')">
<a th:href="@{/moderate}">Moderate</a>
</div>
<!-- āā Display the authenticated user's details: āāāāāāāāāāāāāāāāāāāāāāā -->
<span sec:authentication="name">Username</span>
<span sec:authentication="principal.email">email</span>
<!-- āā CSRF token in forms (auto-included by Spring Security, but explicit): -->
<form method="post" th:action="@{/users}">
<input type="hidden" th:name="${_csrf.parameterName}"
th:value="${_csrf.token}" />
<!-- fields -->
</form>
<!-- Spring Security's Thymeleaf integration adds CSRF automatically
to any th:action form ā the explicit input above is not needed. -->