Thymeleaf 5 min read

Understanding Thymeleaf Syntax and Expressions in Spring Boot with Kotlin

Discover how to effectively use Thymeleaf expressions in your Spring Boot applications. This comprehensive guide covers everything from basic variable expressions to advanced preprocessing, with real Kotlin examples and proven best practices for production applications.

When building web applications with Spring Boot and Kotlin, Thymeleaf serves as a bridge between your dynamic data and your HTML templates.

Understanding Thymeleaf's expression syntax is crucial for creating dynamic, maintainable web pages. In this tutorial, you'll learn how Thymeleaf expressions work, why they're designed the way they are, and how to use them effectively in your applications.

Prerequisites

Before diving in, make sure you have:

  • Basic knowledge of Spring Boot with Kotlin
  • Understanding of HTML and CSS
  • Spring Boot 3.2.0 or later
  • Kotlin 1.9.0 or later
  • An IDE (preferably IntelliJ IDEA)

Introduction to Thymeleaf's Natural Templating

One of Thymeleaf's most distinctive features is its "natural templating" capability. This means that your templates remain valid HTML documents even before being processed. Let's understand why this matters with a simple example:

<!-- Traditional template engine -->
<span>Hello, <%= user.name %>!</span>

<!-- Thymeleaf's natural template -->
<span th:text="${user.name}">John Doe</span>

In the Thymeleaf version, "John Doe" serves as a prototype value that will be replaced with the actual user's name when processed. This approach offers several advantages:

  • Templates can be opened directly in browsers during development
  • Designers can work with realistic-looking prototypes
  • Better IDE support with HTML validation
  • Easier debugging and maintenance

Basic Expression Syntax

Let's start with the fundamental expression types in Thymeleaf. We'll begin with a simple controller to provide context:

@Controller
class UserController {
    @GetMapping("/profile")
    fun showProfile(model: Model): String {
        model.addAttribute("user", User(
            name = "Alice Smith",
            email = "[email protected]",
            isAdmin = true
        ))
        return "profile"
    }
}

data class User(
    val name: String,
    val email: String,
    val isAdmin: Boolean
)

Variable Expressions: ${...}

The most common expression type is the variable expression, which accesses data from your model:

<div class="profile-card">
    <!-- Basic variable access -->
    <h1 th:text="${user.name}">User Name</h1>
    
    <!-- You can also use string concatenation -->
    <p th:text="'Email: ' + ${user.email}">Email: [email protected]</p>
    
    <!-- Conditional rendering using variable expressions -->
    <span th:if="${user.isAdmin}" class="admin-badge">Administrator</span>
</div>

Behind the scenes, ${...} expressions are evaluated using Spring's Standard Expression Language (SpEL). This means you can use powerful features like:

<!-- Method calls -->
<p th:text="${user.name.toLowerCase()}">john doe</p>

<!-- Elvis operator for null safety -->
<p th:text="${user.middleName ?: 'No middle name'}">No middle name</p>

<!-- Collection operations -->
<p th:text="${#lists.size(user.roles)}">3</p>

Selection Expressions: *{...}

Selection expressions work in conjunction with th:object to create a more focused scope for your expressions:

<div th:object="${user}">
    <!-- These are equivalent -->
    <p th:text="${user.name}">John Doe</p>
    <p th:text="*{name}">John Doe</p>
    
    <!-- Nested properties are cleaner with selection expressions -->
    <div th:object="${user.address}">
        <p th:text="*{street}">123 Main St</p>
        <p th:text="*{city}">Springfield</p>
    </div>
</div>

Think of *{...} as saying "from the current object, get this property." It's particularly useful when dealing with form backing objects or complex nested structures.

Link expressions handle URL generation, automatically taking care of context paths and URL encoding:

<!-- Basic URL -->
<a th:href="@{/profile}">Profile</a>

<!-- URL with path variables -->
<a th:href="@{/users/{id}/edit(id=${user.id})}">Edit Profile</a>

<!-- URL with query parameters -->
<a th:href="@{/search(query=${searchTerm}, page=${currentPage})}">Search</a>

Understanding URL expressions is crucial because they handle several complexities:

  • They automatically prepend the application's context path
  • They properly encode URL parameters
  • They support both path variables and query parameters
  • They work consistently across different deployment configurations

Message Expressions: #{...}

Message expressions are used for internationalization (i18n) and externalized text:

// messages.properties
welcome.message=Welcome, {0}!
app.title=My Application

// messages_de.properties
welcome.message=Willkommen, {0}!
app.title=Meine Anwendung
<h1 th:text="#{app.title}">Application Title</h1>
<p th:text="#{welcome.message(${user.name})}">Welcome message</p>

Message expressions are powerful because they:

  • Support parameterized messages
  • Automatically handle locale switching
  • Provide a centralized place for text management
  • Enable easy translation maintenance

Working with Complex Expressions

Real applications often require combining different types of expressions. Let's look at some common patterns:

Conditional Rendering

Thymeleaf provides several attributes for conditional rendering:

<!-- Simple if condition -->
<div th:if="${user.isAdmin}" class="admin-panel">
    <h2>Administrative Controls</h2>
</div>

<!-- If-else using th:unless -->
<div th:if="${user.isVerified}" class="verified-badge">
    Verified User
</div>
<div th:unless="${user.isVerified}" class="unverified-badge">
    Please verify your account
</div>

<!-- Switch statements -->
<div th:switch="${user.role}">
    <p th:case="'ADMIN'">Welcome, Administrator</p>
    <p th:case="'MODERATOR'">Welcome, Moderator</p>
    <p th:case="*">Welcome, User</p>
</div>

Expression Preprocessing

Sometimes you need to process an expression before using it. Thymeleaf's preprocessing syntax allows this:

<!-- Double-brace syntax for preprocessing -->
<div th:with="titleCase=${__${user.name}__.toUpperCase()}">
    <h1 th:text="${titleCase}">JOHN DOE</h1>
</div>

Working with Collections

Thymeleaf provides powerful iteration capabilities:

<div class="user-list">
    <!-- Basic iteration -->
    <div th:each="user, userStat : ${users}" class="user-card">
        <h3 th:text="${user.name}">User Name</h3>
        
        <!-- Accessing iteration status -->
        <span th:text="${userStat.index}">0</span>
        <span th:text="${userStat.count}">1</span>
        <span th:text="${userStat.even}">true</span>
    </div>
</div>

Best Practices and Common Pitfalls

Using Elvis Operator for Null Safety

When working with nullable properties, always use the Elvis operator or safe navigation:

<!-- Bad: Might throw NullPointerException -->
<span th:text="${user.address.street}">123 Main St</span>

<!-- Good: Safe navigation -->
<span th:text="${user?.address?.street}">123 Main St</span>

<!-- Better: Elvis operator with default value -->
<span th:text="${user?.address?.street ?: 'No address provided'}">
    No address provided
</span>

Expression Utility Objects

Thymeleaf provides several utility objects that you should use instead of reinventing common operations:

<!-- Dates -->
<span th:text="${#temporals.format(user.createdAt, 'dd-MM-yyyy')}">
    01-01-2024
</span>

<!-- Numbers -->
<span th:text="${#numbers.formatDecimal(product.price, 1, 2)}">
    19.99
</span>

<!-- Strings -->
<span th:text="${#strings.abbreviate(post.content, 100)}">
    Truncated content...
</span>

Avoiding Common Mistakes

Here are some situations to watch out for:

  1. Forgetting Context Path in URLs:
<!-- Bad: Hardcoded path -->
<a href="/users">Users</a>

<!-- Good: Using link expression -->
<a th:href="@{/users}">Users</a>
  1. Mixing Expression Types:
<!-- Bad: Mixing selection and variable expressions unnecessarily -->
<div th:object="${user}">
    <span th:text="${user.name}">John</span>  <!-- Redundant -->
    <span th:text="*{name}">John</span>       <!-- Better -->
</div>
  1. Not Using Natural Templates:
<!-- Bad: No prototype value -->
<span th:text="${user.name}"></span>

<!-- Good: Includes prototype value -->
<span th:text="${user.name}">John Doe</span>

Conclusion

Understanding Thymeleaf's expression syntax is fundamental to building effective web applications with Spring Boot and Kotlin. By mastering these concepts, you can create more maintainable, robust templates that take full advantage of Thymeleaf's capabilities.

Remember these key points:

  • Use natural templates to maintain HTML validity
  • Choose the appropriate expression type for each use case
  • Leverage utility objects for common operations
  • Consider null safety in your expressions
  • Take advantage of Thymeleaf's preprocessing capabilities when needed

Additional Resources