Publié le 24/02/2026 Source : sfeir.dev

Depuis les débuts du développement web avec Java, les développeurs se sont appuyés sur le modèle MVC (Model-View-Controller) pour structurer leurs applications. De JSP (Java Server Pages) à JSF (Java Server Faces), en passant par des bibliothèques comme Velocity ou Freemarker, les solutions n’ont pas manqué pour relier une logique métier à une présentation web.
Toutefois, ces approches avaient souvent leurs limites : syntaxe peu intuitive, séparation incomplète entre logique et vue, ou encore difficulté à produire du HTML “propre” et conforme aux standards du web moderne.

C’est dans ce contexte qu’est né Thymeleaf, un moteur de template pensé pour la génération de pages HTML dynamiques.
Couplé à Spring Boot, il permet d’allier la puissance et la flexibilité du framework Spring avec une approche simple et élégante de la couche “vue”. Contrairement à ses prédécesseurs, Thymeleaf génère du HTML parfaitement valide, lisible à la fois par un navigateur et par un développeur avant traitement côté serveur.

Présentation de Thymeleaf

Thymeleaf est un moteur de templates côté serveur pour Java. Ses fichiers sont des HTML valides enrichis d’attributs th:*. S’ils sont ouverts directement dans un navigateur, ils restent lisibles ; rendus via Spring, ils deviennent dynamiques (injection de données, conditions, boucles, formulaires liés aux beans, etc.).

Thymeleaf a été créé par Daniel Fernández. Le projet est open source et largement adopté dans l’écosystème Spring.
En raison de sa syntaxe claire et de sa compatibilité avec les standards HTML5, Thymeleaf est non seulement utilisé en production, mais également dans un grand nombre de tutoriels, de formations et de projets d’apprentissage autour de Spring Boot. Cela contribue à en faire un incontournable du développement web Java côté serveur.

[

Thymeleaf

A modern server-side Java template engine for both web and standalone environments. - Thymeleaf

GitHub

](https://github.com/thymeleaf)

projet github Thymeleaf

Thymeleaf et le modèle MVC

Spring Boot repose largement sur le modèle MVC (Model-View-Controller), une architecture qui sépare les responsabilités de l’application en trois couches distinctes :

Thymeleaf s’intègre parfaitement dans ce schéma : les contrôleurs Spring envoient les objets Java (modèle) à la vue, et Thymeleaf se charge de les afficher en HTML.

Ainsi, l’utilisateur interagit avec une page web (vue), les données sont traitées par le contrôleur et les services (modèle), et le cycle se répète.

Comment l’utiliser ?

Dans votre fichier pom.xml ajouter la dépendance suivante

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

Spring Boot configure automatiquement Thymeleaf comme moteur de rendu des vues.

Les fichiers HTML doivent être placés dans le répertoire src/main/resources/templates/ .
Exemple :

Un contrôleur qui retourne "authors" redirige automatiquement vers le fichier templates/authors.html :

@GetMapping("/authors")
public String showAuthors(Model model, @RequestParam(required = false) String keyword) {
    List<Author> authors;
    if (keyword != null && !keyword.isEmpty()) {
        authors = libraryService.searchAuthors(keyword);
    } else {
        authors = libraryService.getAllAuthors();
    }
    model.addAttribute("authors", authors);
    model.addAttribute("keyword", keyword);
    model.addAttribute("newAuthor", new Author());
    return "authors";
}

Les données transmises par le contrôleur sont accessibles en HTML via ${...} :

<ul>
  <li th:each="a : ${authors}" th:text="${a}"></li>
</ul>

Ce code génère une liste <ul> avec tous les auteurs.

<span th:text="${book.title}"></span>
<form th:object="${book}">
  <input type="text" th:field="*{title}" />
</form>

Ici, *{title} pointe automatiquement vers book.getTitle().

<a th:href="@{/books/{id}(id=${book.id})}">Voir le livre</a>
<h1 th:text="#{page.title}"></h1>

Si messages_fr.properties contient page.title=Liste des livres, Thymeleaf affichera cette traduction.

⚖️ Avantages et inconvénients

➕ Avantages

➖ Inconvénients

Exemple pratique

Prenons un exemple simple, une petite application de gestion de bibliothèque, nous aurons 3 écrans :

Un auteurs pouvant écrit plusieurs livres, nous pourrons également lui ajouter des livres dans sa bibliographie.

Le controleur

@Controller
public class LibraryController {

    private final LibraryService libraryService;

    public LibraryController(LibraryService libraryService) {
        this.libraryService = libraryService;
    }

    @GetMapping("/")
    public String home() {
        return "redirect:/authors";
    }

    @GetMapping("/authors")
    public String showAuthors(Model model, @RequestParam(required = false) String keyword) {
        List<Author> authors;
        if (keyword != null && !keyword.isEmpty()) {
            authors = libraryService.searchAuthors(keyword);
        } else {
            authors = libraryService.getAllAuthors();
        }
        model.addAttribute("authors", authors);
        model.addAttribute("keyword", keyword);
        model.addAttribute("newAuthor", new Author());
        return "authors";
    }

    @GetMapping("/books")
    public String showBooks(Model model, @RequestParam(required = false) String keyword) {
        List<Book> books;
        if (keyword != null && !keyword.isEmpty()) {
            books = libraryService.searchBooks(keyword);
        } else {
            books = libraryService.getAllBooks();
        }
        model.addAttribute("books", books);
        model.addAttribute("keyword", keyword);
        return "books";
    }

    @GetMapping("/author/{id}")
    public String showAuthorDetails(@PathVariable Long id, Model model) {
        libraryService.getAuthorById(id).ifPresent(author -> {
            model.addAttribute("author", author);
            Book newBook = new Book();
            newBook.setAuthor(author);
            model.addAttribute("newBook", newBook);
        });
        return "author-details";
    }

    @PostMapping("/author")
    public String addAuthor(@Valid @ModelAttribute("newAuthor") Author author, BindingResult result, Model model) {
        if (result.hasErrors()) {
            model.addAttribute("authors", libraryService.getAllAuthors());
            model.addAttribute("keyword", null);
            return "authors";
        }
        libraryService.saveAuthor(author);
        return "redirect:/authors";
    }

    @PostMapping("/book")
    public String addBook(@Valid @ModelAttribute("newBook") Book book, BindingResult result, @RequestParam Long authorId, Model model) {
        if (result.hasErrors()) {
            libraryService.getAuthorById(authorId).ifPresent(author -> model.addAttribute("author", author));
            return "author-details";
        }
        libraryService.getAuthorById(authorId).ifPresent(book::setAuthor);
        libraryService.saveBook(book);
        return "redirect:/author/" + authorId;
    }
    
    @GetMapping("/delete-author/{id}")
    public String deleteAuthor(@PathVariable Long id) {
        libraryService.deleteAuthor(id);
        return "redirect:/authors";
    }

    @GetMapping("/delete-book/{id}")
    public String deleteBook(@PathVariable Long id) {
        Book book = libraryService.getBookById(id).orElseThrow(() -> new IllegalArgumentException("Invalid book Id:" + id));
        libraryService.deleteBook(id);
        return "redirect:/author/" + book.getAuthor().getId();
    }
}

Les vues Thymeleaf : les attributs th:* en action

###

<form th:action="@{/authors}" method="get" class="form-inline mb-3">
  <input type="text" name="keyword" th:value="${keyword}" class="form-control mr-2" placeholder="Search by name"/>
  <button type="submit" class="btn btn-primary">Search</button>
</form>
<tr th:each="author : ${authors}">
    <td th:text="${author.id}"></td>
    <td th:text="${author.name}"></td>
    <td>
        <ul>
            <li th:each="book : ${author.books}" th:text="${book.title}"></li>
        </ul>
    </td>
    <td>
        <a th:href="@{/author/{id}(id=${author.id})}" class="btn btn-primary">View</a>
        <a th:href="@{/delete-author/{id}(id=${author.id})}" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this author?')">Delete</a>
    </td>
</tr>

<div class="modal-body">
  <form th:action="@{/author}" th:object="${newAuthor}" method="post">
      <div class="form-group">
          <label for="name">Name:</label>
          <input type="text" th:field="*{name}" id="name" class="form-control" required/>
          <div th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="text-danger"></div>
      </div>
      <button type="submit" class="btn btn-primary">Submit</button>
  </form>
</div>

🗣️ i18n : placez vos messages dans messages.properties (ex. Author name cannot be empty=Le nom de l’auteur est obligatoire). Puis utilisez #{…} dans la vue si besoin.

//TODO ajouter lien vers article sur la i18n et l10n

Je vous passe la logique de la page books, c’est exactement la même chose.

Les alternatives

Selon le projet et le style d’interface attendu, plusieurs voies existent :

Papa, je veux un Pokédex - partie 2

Conclusion

Thymeleaf + Spring Boot incarne une approche classique et solide du web serveur : claire, maintenable, et parfaitement intégrée à l’écosystème Spring. Les attributs th:* permettent de lier naturellement vos données Java aux vues HTML, avec un support élégant des formulaires et de la validation.

Pour des applications administratives ou des sites nécessitant un rendu serveur fiable et maîtrisé, c’est un choix sûr. Pour des UI très riches et interactives, regardez du côté des SPA ; pour rester intégralement en Java avec des composants haut niveau, Vaadin est une alternative de premier plan.