Using Grails with Thymeleaf for a Flexible View Layer
For many teams across Sydney and Melbourne, Grails remains a productive way to build web applications on the JVM, but the default GSP view technology sometimes feels limiting when designers want to preview pages in a browser without spinning up the full server. Thymeleaf solves that problem with natural HTML templates that render correctly even when opened directly, which makes hand-offs to front-end colleagues smoother and reduces the constant context switching that slows down a development cycle.
This walk-through covers the practical steps for wiring Thymeleaf into a Grails project, from plugin selection and bean configuration through template authoring and migration of existing views. Along the way, the discussion touches on deployment patterns that suit Australian cloud regions and the small workflow tweaks that matter when the rest of the team is used to GSP tags and layouts.
Why teams reach for Thymeleaf
GSP has served the Grails community well for years, yet several recurring frustrations push developers toward alternatives. Tag libraries feel heavy to front-end staff who rarely touch the back end, layout inheritance through the applyLayout mechanism can surprise newcomers, and the lack of a true offline rendering mode means designers cannot validate markup until a local server is running. Thymeleaf, by contrast, treats templates as plain HTML5 with attribute-based expressions, so any HTML editor can preview the page and the rendered output stays identical in production.
For Australian teams that frequently collaborate with contractors in different time zones, the offline-preview advantage is more than a convenience. A contractor in Perth can hand a static HTML file to a designer in Brisbane without needing a running application, and the designer sees the exact structure that will ship. That level of parity matters when sprint reviews happen late in the arvo and a quick visual check is needed before the next stand-up.
Adding the Thymeleaf plugin
The starting point is the official Thymeleaf plugin for Grails, available from the plugin portal and Maven Central. In a typical build.gradle file, the dependency block ends up looking like a runtime dependency on org.thymeleaf:thymeleaf-spring5 plus the Grails-specific integration library that bridges the view resolver to Spring's DispatcherServlet. Once Gradle resolves the artifacts, the next step is to confirm that the plugin entry is picked up by the application context, which usually happens automatically but occasionally needs a manual entry in application.yml under the plugins section.
If the project is older and still uses BuildConfig.groovy, the equivalent declaration in that file pulls the plugin from the central repository. Developers who already manage infrastructure through code, like the workflow described in the Grails with Terraform guide, will appreciate that the plugin configuration lives in the same declarative files they already version-control alongside their provisioning scripts.
Wiring the Spring view resolver
With the plugin on the classpath, Grails still expects a ViewResolver bean to decide which engine handles each request. The Thymeleaf plugin registers a SpringTemplateEngine and a ThymeleafViewResolver automatically, but a few beans are worth overriding in grails-app/conf/spring/resources.groovy to control template mode, cache settings, and the location of the view directory. Most teams switch the template mode to HTML, point the resolver at WEB-INF/views/ or grails-app/views/, and explicitly set the character encoding to UTF-8 so that accented characters in customer names render correctly.
A common refinement is to register an additional LinkBuilder so that URL helpers such as g:link continue to work inside Thymeleaf templates. Without that custom dialect, every link in the application would have to be written by hand, which quickly defeats the purpose of using Grails. Once the resolver bean is in place, a quick restart and a visit to a known page confirm whether the configuration is correct; if the response comes back as plain text or a 500 with a TemplateInputException, the resolver is usually pointing at the wrong folder.
Authoring your first Thymeleaf template
A Thymeleaf template looks like ordinary markup with a few extra attributes, and that is precisely the point. A simple index.html placed under grails-app/views/ could greet a logged-in user with markup like <p th:text="${user.fullName}">placeholder</p>, where placeholder is what the browser shows when the file is opened outside the application. Designers in Adelaide who occasionally moonlight on back-office tools find this approach much friendlier than GSP's ${} syntax, because the placeholder text keeps the layout stable while data is missing.
The four core expression types cover almost every rendering need:
${...}for variable output from the controller model*{...}for form-binded selection against a command object#{...}for message bundle lookups during internationalisation@{...}for URL building with context path support
Conditional fragments use th:if and th:unless, loops use th:each, and fragments defined in shared files can be included with th:replace or th:insert. A typical list of values rendered as table rows looks identical to static HTML with a single th:each="item : ${items}" attribute on the <tr> tag, which keeps the cognitive load low for anyone who already knows HTML.
Forms, model data, and server-side bindings
Form handling is where the alternative engine shows its maturity. Thymeleaf's th:object and th:field attributes bind a form to a command object exposed by the controller, so the rendered HTML carries the field name, the current value, and a hidden CSRF token without any custom tags. Server-side validation errors surface through the standard BindingResult, and the template can show them through th:errors or a custom fragment. For teams that previously wrote GSP forms by hand, the experience feels familiar but more disciplined.
The Spring expression language integrates directly with Grails domain classes, so a property path such as th:field="*{address.street}" resolves against a nested command object without extra wiring. When a controller returns a map of variables, every key becomes available as a context object inside the template, and Thymeleaf's selection-variable mechanism keeps nested selections efficient even on large forms. The net effect for Australian development shops that handle government or banking forms is a clear path to compliant, accessible HTML with little extra effort.
Migrating GSP views to Thymeleaf
The migration rarely happens in one big bang, and most teams move folder by folder, starting with marketing pages and read-only dashboards before touching complex forms. The first mechanical change is replacing custom <gsp:...> tags with their Thymeleaf counterparts, then swapping layout references from applyLayout to th:replace fragments. Custom tag libraries written in TagLib.groovy files often need a parallel Thymeleaf dialect, but the bulk of simple pages translate with a search-and-replace pass followed by manual cleanup of dynamic attributes.
Attributes that ease the migration step:
th:textreplaces GSP's default body interpolationth:utexthandles unescaped output for trusted content blocksth:hrefandth:srcbuild URLs with proper context pathsth:attrsets arbitrary HTML attributes in one place
After each folder migrates, the test suite should still pass, and visual regression tooling can catch layout drift introduced by subtle differences in whitespace handling. A staged migration also lets product owners compare the old and new experiences side by side, which has been a useful talking point during user research sessions in Canberra offices where accessibility audits are common.
Deployment, caching, and the AWS Sydney region
Production behaviour of Thymeleaf under Grails depends heavily on template caching. By default the engine caches parsed templates in memory for the lifetime of the application context, which is exactly what most teams want in production but a source of confusion during development. Setting spring.thymeleaf.cache=false in application.yml for local profiles, and leaving it true for prod, prevents the classic problem where a code change appears not to have taken effect. Container builds through Docker, JAR layouts, or the traditional WAR file all behave the same way once the resolver bean is configured correctly.
Deployment to the AWS Sydney region (ap-southeast-2) is a sensible default for Australian customers because of latency and data-residency expectations, and the same WAR or fat JAR deploys without modification. Teams that automate provisioning can reuse the patterns from the Terraform guide linked earlier in this article, and they can document the cache setting in the same place they record environment variables. When a deployment rolls out across multiple availability zones in Sydney, the cache reset on application restart is usually fast enough that no warm-up traffic is required, but a health-check endpoint that hits a known template is still a worthwhile safeguard.
Thymeleaf offers a clean, designer-friendly way to render HTML in a Grails application without abandoning the Spring ecosystem that makes Grails productive in the first place. The migration is incremental, the runtime behaviour is predictable, and the templates work in any HTML editor your team already uses. Combined with the existing Grails plugin model, the path from GSP to Thymeleaf is shorter than many teams expect.
If you would like to deepen your command-line skills while moving templates around on a server, the Linux command reference provides a useful companion for the deployment half of the work, from checking process status to inspecting running JARs. Subscribe to the Grails Example newsletter for the next instalment, where we look at internationalisation with Thymeleaf message bundles and at caching strategies that work with edge deployments across Australian data centres.