Best Practices for Thymeleaf and Spring Boot
Thymeleaf is the most popular template language in Spring Boot. This article provides a set of best practices to put your own application on a solid foundation to be productive and happy in the long run.
#1 Structure your templates
Where exactly should the templates be located that are rendered by our controllers? It is recommended to use the directory /<ControllerName>/<MethodName>.html
for this purpose. This way every developer knows directly where a needed template can be found and how its name will be.
@Controller
public class HomeController {
@GetMapping("/pricing.html")
public String pricing() {
return "home/pricing";
}
}
▴ Example controller targeting a Thymeleaf template
The template of our example endpoint would be located in /resources/templates/home/pricing.html
- we leave out the controller
suffix. Additional included components that are not used by a controller could be stored in a folder /resources/templates/fragments
.
#2 Work with layouts
Usually there are groups of pages in the project that have the same overall page structure. To manage the HTML code centrally in one place, we can use the Thymeleaf Layout Dialect. After we include the nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect
dependency in our project (pom.xml
/ build.gradle
/ build.gradle.kts
) the plugin is ready for use.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout}">
<head>
<title th:text="#{home.pricing.headline}" />
</head>
<body>
<!-- actual page content goes here -->
</body>
</html>
▴ Using the layout dialect in one of our templates
The given example could be the template pricing.html
of our HomeController
from above. It will reuse the existing layout.html
, extend the page title and provide the actual content within its <body>
tag.
#3 Use fragments for your form inputs
In our web application we will certainly have some forms that a user can fill out. Thereby the look and functionality should be consistent without copying complex elements all the time. For this we define ourselves a fragment inputRow
with parameters object
, field
, type
(optional) and required
(optional).
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<body>
<div th:fragment="inputRow(object, field)" th:with="type=${type} ?: 'text', required=${required}, inputClass=${inputClass}" th:object="${__${object}__}">
<div th:if="${type == 'checkbox'}">
<div>
<input th:type="${type}" th:field="*{__${field}__}" th:classappend="${#fields.hasErrors(field) ? 'is-invalid' : ''} + ' ' + ${inputClass}" />
<label th:for="${#ids.prev(field)}">
<span th:text="#{__${object}__.__${field}__.label}" />
</label>
</div>
<div th:replace="~{:: fieldErrors(${object}, ${field})}" />
</div>
<label th:if="${type != 'checkbox'}" th:for="${field}">
<span th:text="#{__${object}__.__${field}__.label}" />
</label>
<div th:if="${type != 'checkbox'}">
<input th:if="${type == 'text' || type == 'password' || type == 'email' || type == 'tel' || type == 'number'}"
th:type="${type}" th:field="*{__${field}__}" th:classappend="${#fields.hasErrors(field) ? 'is-invalid' : ''} + ' ' + ${inputClass}" />
<!-- ... -->
</div>
</div>
<!-- ... -->
</body>
</html>
▴ Section of the inputRow fragment
With object
and type
the data transfer object and the desired field are specified. This enables binding the data transfer object from the model to the form field - providing its current value and field errors later on. As object
and field
are strings, this information can also be used for further context information like the label. With type
you can specify the type (fallback text
- a classic input field) and with required
you can override the required status.
At the beginning you have to invest some time to configure the different types in your own project. In Bootify's Free Plan all form elements are provided if a Thymeleaf frontend and a CRUD option has been activated.
<form th:action="@{'/books/add'}" method="post">
<div th:replace="~{fragments/forms::inputRow(object='book', field='title', required='true')}" />
<div th:replace="~{fragments/forms::inputRow(object='book', field='author')}" />
<div th:replace="~{fragments/forms::inputRow(object='book', field='year', type='number')}" />
<input type="submit" th:value="#{book.add.headline}" />
</form>
▴ Example form using our new fragment
We can now use this fragment to structure our forms in a very compact way. Required changes can be made centrally in one file and will be applied automatically to all forms.
# 4. Use Hot Reload during development
Even if you disable caching with spring.thymeleaf.cache=false
, changed templates must first be recompiled to be visible in the browser. Even though this only takes up to a few seconds, it adds up to a lot of time per developer per day.
Instead, you can configure the TemplateEngine
for development so that all templates are loaded directly from the file system using the FileTemplateResolver
. This way all changes are immediately visible in the browser.
@Configuration
@Profile("local")
public class LocalDevConfig {
public LocalDevConfig(final TemplateEngine templateEngine) throws IOException {
final FileTemplateResolver fileTemplateResolver = new FileTemplateResolver();
// ...
templateEngine.setTemplateResolver(fileTemplateResolver);
}
}
▴ Extract of the LocalDevConfig
More backgrounds and the full configuration of the resolver can be found in this article.
With Bootify, advanced Spring Boot applications can be initialized with their custom database schema, CRUD functions and many more features - without registration directly in the browser. If a Thymeleaf frontend has been selected, all best practices are applied: the layout dialect, the form fragments and the LocalDevConfig
, matching exactly the chosen setup.