Server-Side Templating with Mustache in Spring Boot: A Practical Guide
Learn to use Mustache templates in Spring Boot for server-side rendering.
20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.
- ✓Java 17+ installed on your machine
- ✓Spring Boot 3.2+ project with spring-boot-starter-web dependency
- ✓Basic understanding of Spring MVC (@Controller, Model, @GetMapping)
• Mustache is a logic-less, server-side Java template engine integrated with Spring Boot via spring-boot-starter-mustache • It enforces separation of concerns by disallowing complex logic in templates, making it ideal for simple view rendering • Use partials and layouts for code reuse, and rely on Spring MVC's Model for data binding • Avoid common pitfalls like unescaped HTML, missing partials, and incorrect context paths in production
Think of Mustache as a fill-in-the-blanks form for your web pages. You write the static HTML skeleton with placeholders like {{name}}, and Spring Boot fills those placeholders with actual data before sending the page to the user's browser. Unlike JSP, Mustache won't let you write Java code in the template, which keeps your designers and developers from stepping on each other's toes.
Server-side templating is the backbone of traditional web applications, and despite the rise of SPAs, it's still the right choice for many scenarios: SEO-critical pages, simple dashboards, or when you need to render HTML fast without client-side JavaScript overhead. Mustache, with its logic-less philosophy, is a breath of fresh air compared to the spaghetti code you often see in JSP or Thymeleaf templates. In this guide, I'll show you how to integrate Mustache with Spring Boot 3.2, structure your templates for maintainability, and avoid the landmines I've stepped on in production over the years. We'll cover setup, partials, layouts, form handling, and debugging techniques. By the end, you'll be able to build clean, maintainable server-rendered UIs that won't make you want to quit development. I've used Mustache in payment-processing dashboards and SaaS billing portals where page load time is critical, and it's never let me down. Let's get our hands dirty.
Setting Up Mustache in Spring Boot 3.2
Adding Mustache to your Spring Boot project is trivial. Just add the spring-boot-starter-mustache dependency to your pom.xml or build.gradle. Spring Boot's auto-configuration will detect the starter and configure a MustacheViewResolver for you. By default, templates go in src/main/resources/templates/ with a .mustache extension. You don't need to write any Java configuration for basic usage. Here's a minimal controller and template. Note that Mustache is logic-less, so you cannot use if/else statements directly; you must prepare the data in the controller and use boolean flags or lists in the template. This forces you to keep business logic in Java, which is a good thing. I've seen teams try to hack around this with lambdas, but don't. Keep it simple.
What the Official Docs Won't Tell You
The official Spring Boot docs tell you how to start, but they skip the painful parts. First, Mustache's partial resolution is brittle. If you use a partial like {{> header}}, the file must be header.mustache in the same templates directory (or a subdirectory relative to the calling template). There's no fallback. If the partial is missing, you get a silent blank output, not an error. Second, context path handling: if your app runs behind a reverse proxy with a context path (e.g., /myapp/), you must manually prepend that path to links and resources. Spring Boot's server.servlet.context-path property does not automatically affect Mustache templates. Use a custom helper or pass the context path as a model attribute. Third, Mustache's default HTML escaping is aggressive. It will escape single quotes, which breaks some JavaScript. I've seen production bugs where JSON embedded in a script tag was corrupted because Mustache escaped quotes. Use triple mustache for raw JSON, but then you risk XSS. The solution is to use a dedicated JSON serializer that produces safe output. Fourth, there is no built-in support for layout composition (like Thymeleaf's fragments). You need to implement your own layout pattern using partials or a custom ViewResolver. I'll show you a layout pattern in the next section that saved my team from template duplication hell.
Partials and Layouts for Code Reuse
In any non-trivial application, you'll have repeated UI elements: headers, footers, navigation bars. Mustache's partials are the way to reuse these. A partial is just another .mustache file that you include with {{> partialName}}. The partial has access to the same model as the parent template. For layouts, I use a pattern where the main template is a layout that includes a content partial. The controller specifies which content partial to use via a model attribute. Here's the layout template (layouts/default.mustache) and an example page template. This approach keeps your templates DRY. In production, I've seen teams with 50+ templates all duplicating the same HTML structure. This pattern eliminates that. Note that partials cannot pass parameters directly; they inherit the entire model. If you need scoped data, put it in the model under a specific key. Also, avoid deep nesting of partials — it becomes impossible to debug. Max two levels.
Form Handling and CSRF Protection
Mustache works fine with Spring MVC forms, but you need to handle CSRF tokens manually. Spring Security automatically adds a CSRF token to the request, but Mustache doesn't have a built-in way to insert it into forms. You must add the token as a model attribute. Since Spring Security 6.x, the default CsrfToken is available as a request attribute. In your controller, you can extract it and add it to the model. Alternatively, use a @ControllerAdvice to add it to all models. Here's how to include the CSRF token in a form. Never skip CSRF protection in production, especially in payment-processing apps. I once audited a system that had CSRF disabled for "simplicity" — it was a disaster waiting to happen. Also, handle validation errors by re-populating the form with the submitted values and error messages. Mustache's section syntax ({{#errors}}) works well for displaying validation errors.
Debugging Mustache Templates in Production
Debugging Mustache templates is painful because errors often result in blank pages or silently ignored sections. The Mustache compiler doesn't throw exceptions for missing variables — it just outputs nothing. This is by design (logic-less), but it makes debugging a nightmare. Here's my toolkit: First, enable Spring Boot's debug logging for Mustache: logging.level.org.springframework.web.servlet.view.mustache=DEBUG. This logs template resolution and rendering. Second, use a custom Mustache.Compiler with error handling. Third, in development, set spring.mustache.servlet.expose-request-attributes=true and expose session attributes to see all available data. Fourth, never use {{! comments }} for debugging; they are stripped. Instead, temporarily output the variable name: {{variableName}} = {{variableName}}. Fifth, if a partial is not rendering, check the file name and path. The most common issue is a typo in the partial name or the file not being in the expected directory. I've created a utility that logs all model attributes before rendering — it's saved me hours. Finally, use a template that renders all model attributes as a debug page. This is invaluable in production when a user reports a weird display.
Performance Optimization and Caching
Mustache is fast, but you can still shoot yourself in the foot. The default Mustache compiler compiles templates on first use. In production, this is fine because Spring Boot caches compiled templates. But if you have many templates (100+), the initial compilation can cause a startup delay. Pre-compile templates by setting spring.mustache.prefix=classpath:/templates/ and spring.mustache.suffix=.mustache (defaults). For high-traffic pages, consider using Mustache's buffered rendering mode. Also, avoid putting large lists in the model that are not displayed — they still get serialized. In one project, we had a model attribute that contained a full audit log (10,000 entries) but the template only showed the first 10. The rendering was slow because Mustache iterated the entire list. The fix was to pass only the needed subset. Another tip: use the {{.}} syntax for iterating over strings, but be careful with performance. For maximum throughput, I've used Mustache with reactive stacks (Spring WebFlux), but that's advanced. For most apps, the default setup is fine. Just ensure you have adequate heap for template caching. In a payment-processing system handling 1000 requests/second, Mustache added less than 2ms per request.
Internationalization (i18n) with Mustache
Mustache doesn't have built-in i18n support, but you can easily integrate Spring's MessageSource. The trick is to expose a message resolver as a lambda in the Mustache context. Spring Boot's auto-configuration does not do this automatically, so you need to register a custom Mustache.Compiler that adds a 'msg' function. Alternatively, you can add all messages as model attributes, but that's messy. I prefer the lambda approach. Here's how to set it up. In your templates, you can then use {{#msg}}key{{/msg}} to render localized strings. Note that Mustache lambdas are not thread-safe by default, so make sure your lambda implementation is stateless. In a multi-tenant SaaS app, we had to switch locales per request. We stored the locale in the request scope and used a ThreadLocal in the lambda. This worked, but be careful with thread pools. For simple apps, you can just add the messages as model attributes from a @ControllerAdvice. That's less elegant but works. I've seen teams abandon Mustache for Thymeleaf just because of i18n, but that's overkill. The lambda approach is clean and maintainable.
Testing Mustache Templates with Spring Boot
Testing templates is often overlooked until a production bug hits. You should test both that the template compiles and that it renders the expected HTML. Spring Boot provides MockMvc for this. Use MockMvc to perform a request and assert the response content. For more advanced testing, use HtmlUnit or jsoup to parse the HTML and assert on specific elements. Never test the rendered output with string equality — it's brittle. Instead, use XPath or CSS selectors. Also test edge cases: null values, empty lists, and special characters. I've seen templates crash because a list was null and the template tried to iterate it. In Mustache, {{#list}}...{{/list}} handles null gracefully (section is skipped), but {{list.name}} will throw a NullPointerException. Always check for null in the controller or use a wrapper object. Another tip: test that your partials are resolved correctly by using a mock view resolver. Finally, include a test that verifies CSRF token is present in forms. Here's a sample test using MockMvc and jsoup.
The Case of the Escaped Credit Card Numbers
- Always audit which template variables contain user-generated or special-character data and use triple mustache explicitly for those.
- Add integration tests that verify rendered HTML contains the exact expected output, not just the model data.
logging.level.org.springframework.web.servlet.view.mustache=DEBUGcurl -v http://localhost:8080/your-endpoint| File | Command / Code | Purpose |
|---|---|---|
| InvoiceController.java | @Controller | Setting Up Mustache in Spring Boot 3.2 |
| LayoutHelper.java | @ControllerAdvice | What the Official Docs Won't Tell You |
| layouts | Partials and Layouts for Code Reuse | |
| SecurityConfig.java | @Configuration | Form Handling and CSRF Protection |
| DebugInterceptor.java | public class DebugInterceptor implements HandlerInterceptor { | Debugging Mustache Templates in Production |
| application.yml | spring: | Performance Optimization and Caching |
| MustacheConfig.java | @Configuration | Internationalization (i18n) with Mustache |
| InvoiceControllerTest.java | @SpringBootTest | Testing Mustache Templates with Spring Boot |
Key takeaways
Interview Questions on This Topic
Explain how Mustache's section syntax works and how it differs from Thymeleaf's th:if.
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.
That's Spring Boot. Mark it forged?
5 min read · try the examples if you haven't