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.

» Start Project on Bootify.io  

Further readings

Thymeleaf Layout Dialect
Thymeleaf Fragments