In the first post in this blog series, we looked at the benefits and drawbacks of build-time checks and code generation for HTTP request routing and reverse routing. In this part, we will look at the next logical part of the web stack — at least if you grew up with the now retro fashion of generating HTML on the server and sending it to the browser: HTML templating.

Characteristics of mainstream Java template engines

As mentioned in the previous blog post, the general trend in our industry is currently towards the aesthetics of static and typing. In the TypeScript world, TSX is an example of a popular template language that has been influenced by this trend. In the Java community, most of the widely used template engines have resisted this trend and show the following characteristics:

The promises of compiled templates

While far from mainstream in the Java community, there are a few Java template engines that do use static typing, code generation, and build-time verification of templates. Maybe this approach is not that popular in the Java community because Java has a history with compiled HTML templates: 25 years ago, we saw the birth of Java Server Pages (JSP), which were compiled to Java servlets — usually the first time they were used after application start, but it was possible to pre-compile them during build time to improve performance and find errors early. While JSP was the de-facto standard for a long time, it had a lot of critics, too.

There were good reasons to shun JSP. However, this doesn’t imply that the general idea of compiled HTML templates is bad per se. Maybe JSP was just not a good implementation of the idea.

What are the problems that compiled, type-safe templates are trying to solve? If we use static typing in our template engine for its own sake, because it’s en vogue, then that’s just not good enough. What do we hope to get from this that we cannot get from dynamically typed templates that are parsed at runtime? Depending on your individual needs, you might be interested in one or more of the following.

Performance

A template engine that parses its templates at runtime cannot make the same optimizations as a template engine that compiles its templates to Java classes. In addition, there is a performance penalty of using reflection when accessing objects and their properties passed to the template during rendering. If you have or expect a performance problem with rendering HTML, this may be a valid reason to look into compiled templates.

Minimal effort when targeting GraalVM

If a template engine relies on reflection, this usually entails a lot of effort when targeting GraalVM. If you intend to run your application on GraalVM, this, too, may be a valid reason to look for alternatives to reflection-based template engines.

Preventing bugs early

If HTML templates are compiled to Java classes or verified in some other way at build-time, you can be sure that a certain category of bugs will be prevented — specifically, bugs caused by passing the wrong type of variable to a template, or from creating templates that that refer to non-existing properties of objects passed to it.

Some of these errors can be caught early as well with template engines that parse their templates at runtime and use dynamic typing. Others, however, will probably slip through and only lead to unexpected HTML output — which may or may not be caught by your safety net of browser tests.

Decent support by your IDE

In a way, this is a sub category or specialization of the previous point. Only if you have a statically typed contract between your view model and the template it renders, is there a way for your IDE to help you. It can make use of what it knows about this typed contract in order to show you an error if you pass variables of the wrong type to the template. It can also show you an error in your template if you refer to a non-existing property of an object passed to the template. Finally, this contract allows your IDE to offer autocomplete suggestions when calling the template or editing the template and referring to properties of objects. This way, bugs are prevented even earlier, before the build-time verification.

Support for component-based approach

It’s 2024, and it’s common to use a component-based approach to producing HTML. Often, reusable HTML components are shared in a pattern library. These HTML snippets will have to be translated to the template language of choice for each team. On top of the pattern library, the components can be shared in a component library using a specific template engine.

A component must have some contract with the HTML template using it, just like the view model has a contract with the HTML template. Ideally, the contract between a component and the template using it is statically typed as well and not just a matter of documentation. We want to ensure that our HTML templates call reusable components correctly, passing all the variables to the component that it needs, and that those variables are of the correct type. In our wildest dreams, our IDE will even autosuggest variables that we need to pass to the component in our template.

Can Java template engines deliver?

Let’s see what’s possible in the Java ecosystem today and how this compares to what the Rust community has come up with. We’ll also look at the benefits and downsides of the various approaches towards HTML templates that are verified at build-time and don’t rely on reflection.

Twirl

The Play framework is probably the only well-known web framework whose default template engine, Twirl, makes use of static typing. Twirl itself is not really a Java template engine, but rather a Scala template engine. Nevertheless, since Play has well-supported Java APIs and makes using Twirl with Java quite easy, it’s worth presenting it here.

Twirl templates can make use of embedded Scala expressions, and they have a constructor with a parameter list, in which you need to specify all the parameters that the template uses. A simple Twirl template looks like this:

@(name: String)  
<html lang="en">
<head>
    <title>Hello world</title>
</head>
<body>
    <h1>Hello @name!</h1>  
</body>
</html>

At build-time, Scala source files are generated from Twirl templates, which are then compiled like all other source files. In your Play Java application, you can then call the render method on the generated Scala object, just as you would on any other Java or Scala object:

package com.example;  
  
import play.mvc.Controller;  
import play.mvc.Result;  
  
public class HelloController extends Controller {
    public Result hello(String name) {
        return ok(web.views.html.hello.render(name));
    }  
}

Since Twirl templates are effectively Scala objects, you will catch a lot of typical errors at build-time. If your template has a syntax error, it won’t compile. If it tries to access a variable or a property of a variable that doesn’t exist, it won’t compile. If you try to render a template that doesn’t exist, this won’t compile.

For all of these issues, you will get feedback directly in your IDE, which can also help you with code completion, both when editing a template and when it comes to rendering a template.

You can define reusable components as Twirl templates as well. These are compiled like all other Twirl templates, and you call them from other Twirl templates in a way that is very similar to how you call them from Java. Again, your IDE will show you errors if you violate the contract between the component and the calling template, and it will offer you code completion to prevent this from happening in the first place.

Hot reloading is possible, too. If you change a template, the corresponding source file is generated and compiled again in a locally running Play application. You will get helpful error messages if you violate any contracts or have a syntax error in your templates. However, since the Scala compiler is not the fastest, hot reloading is rather slow, especially for non-trivial templates.

Rocker

Rocker is not much younger than Twirl, looks very similar, and follows almost the same approach. The major difference is that instead of generating Scala source files, Rocker generates a Java source file for each template.

Unlike Twirl, it has never been adopted as the default template engine of any web framework with the prevalence of Play. With Rocker, our example looks like this:

@args (String name)  
<html lang="en">  
<head>
    <title>Hello</title>  
</head>  
<body>
    <h1>Hello, @name!</h1>  
</body>  
</html>

We can then pass parameters to the Hello template using the template method of the generated Hello class and call the render method to render our HTML output:

package com.example.demo.web;  
  
import jakarta.ws.rs.GET;  
import jakarta.ws.rs.Path;  
import jakarta.ws.rs.Produces;  
import jakarta.ws.rs.core.MediaType;  
  
@Path("/hello")  
public class HelloWorldResource {
    
    @GET
    @Produces(MediaType.TEXT_HTML)
    @Path("/{name}")
    public String world(String name) {
        return views.Hello.template(name).render().toString();
    }  
}

Rocker is well-positioned as an alternative if you like the underlying concept of Twirl but want to avoid pulling in Scala and using sbt as a build tool. Like Twirl, it is well-suited for a component-based approach, because partials are compiled to Java classes like ordinary templates, and are called from your template in a type-safe manner.

Unlike Twirl, Rocker makes use of reflection internally, merely providing a type-safe interface.

While Rocker has a documented hot reloading feature, I failed to get it working.

JStachio

One template engine that uses a completely different approach from Rocker and Twirl is JStachio. Whereas the former two each come with their own template language, JStachio uses an existing template language — Mustache — and adds compile-time verification. The first piece you need when using JStachio is an HTML template. This looks like an ordinary Mustache template:

<html lang="en">  
<head>
    <title>Hello world</title>  
</head>  
<body>
    <h1>Hello, {{name}}!</h1>  
</body>  
</html>

In addition, we need a view model class with a @JStache annotation, which connects our template with the variables we need to pass to it:

package com.example.demo.web;  
  
import io.jstach.jstache.JStache;  
  
@JStache(path = "hello.mustache")  
public record GreetingModel(String name) {  
}

The view model class needs to contain all the fields referenced in the template. The annotation tells JStachio where to look for the Mustache template. It will then generate a Java source file called GreetingModelRenderer based on our Mustache template and the annotated view model class. This renderer can be used like this:

package com.example.demo.web;  
  
import jakarta.ws.rs.GET;  
import jakarta.ws.rs.Path;  
import jakarta.ws.rs.Produces;  
import jakarta.ws.rs.core.MediaType;  
  
@Path("/hello")  
public class GreetingResource {  
    
    @GET()
    @Produces(MediaType.TEXT_HTML)
    @Path("/{name}")
    public String world(String name) {
        return GreetingModelRenderer.of().execute(new GreetingModel(name));
    }  
}

You will get a compile error if you pass the wrong parameters to the renderer, or miss any parameters. However, JStachio does not support defining a contract between a partial or component and a regular template, because Mustache doesn’t have the concept of statically typed partials.

Also, JStachio has no built-in support for hot reloading, and, just as with Rocker, your IDE will not automatically pick up changes in your Mustache templates and generate the respective renderer source file again.

Qute

One other interesting option in this space is Qute, a template engine that is part of the Quarkus project. While Qute can be used as a standalone template engine, its tight integration with Quarkus is reminiscent of the relationship between Twirl and Play. A simple Qute template looks like this:

<html lang="en">  
<head>
    <title>Hello world</title>  
</head>  
<body>
    <h1>Hello, {viewModel.name}!</h1>  
</body>  
</html>

A Qute template can expect more than one variable being passed to it, but in this case, we bundle all the context needed in the template in properties of an object assigned to the name viewModel.

Qute has a feature called type-safe templates. For these, in addition to the HTML template, we need a class or record annotated with @CheckedTemplate. Each static native method needs an HTML template file with the name of the respective method. We define both our ViewModel and the annotated class as static inner classes of our resource class:

package com.example.demo.web;  
  
import io.quarkus.qute.CheckedTemplate;  
import io.quarkus.qute.TemplateInstance;  
import jakarta.ws.rs.GET;  
import jakarta.ws.rs.Path;  
import jakarta.ws.rs.Produces;  
import jakarta.ws.rs.core.MediaType;  
  
@Path("/hello")  
public class HelloResource {
    
    @CheckedTemplate
    public static class Templates {
        public static native TemplateInstance hello(ViewModel viewModel);
    }  
  
    private record ViewModel(String name) {}  
  
    @GET
    @Produces(MediaType.TEXT_HTML)
    @Path("/{name}")
    public TemplateInstance hello(String name) {
        return Templates.hello(new ViewModel(name));
    }  
}

We only define the interface for rendering our template. In this case, we define a static native method called hello that expects a ViewModel. The return type of these methods is always TemplateInstance.

At compile-time, we can be sure that we pass all the parameters of the correct type to our template when we want to render it.

However, the connection to the actual HTML template does not happen at compile-time, but in a Quarkus build step. Quarkus build steps are not part of the compilation phase, but are executed after compilation. This means that the verification of the template happens outside of the Java compiler and outside of the realm of the Java type system.

This means that if you, for example, access a variable of the wrong name in your hello.html template, the Java compiler will not tell you. Your IDE that relies on Java compilation will not tell you either. Since the templates are regular HTML files to your IDE, it won’t support you with autocompletion for properties of the variables passed to the template either. The latter is true for all the other templates we covered as well, apart from Twirl.

Quarkus build steps are executed when you call quarkus build, but also whenever there is a change while you are running the application locally using quarkus dev. When using quarkus dev, hot reloading of Qute templates works like a charm. Any change you make to a template is picked up immediately when you refresh your browser, and any errors in your templates lead to meaningful error messages in the browser or in your console. Basically, this means that you will rely less on your IDE and more on what you see in your browser or in your console. That may not be a problem if you have already decided to go all in on Quarkus and are ready to adapt your development workflow to it.

Unlike Twirl or Rocker, there is no way in Qute to define a statically-typed signature for a re-usable component. Build-time verification is only supported for the contract between your view model and the HTML template.

Beyond Java

Now that we have seen what the Java ecosystem has to offer in terms of build-time verified HTML templates, let’s turn to Rust. Can its macros and its stronger type system broaden the design space for HTML template engines?

Type-safe templates with Askama

Most Rust template engines follow the traditional approach outlined at the beginning of the article. One of the engines using build-time verification and template compilation is Askama. It uses an approach that is similar to JStachio. Instead of inventing a completely new template language, it uses the familiar Jinja2 syntax. Just like JStachio, Askama templating consists of two elements: the actual HTML template and an annotated type. The template is an ordinary Jinja2 template:

<html>
<head>
    <title>Hello world</title>
</head>
<body>
    <h1>Hello, {{name}}!</h1>
</body>
</html>

The template uses one variable, name. The second element is a struct that has a corresponding field:

use askama_rocket::Template;

[derive(Template)]
#[template(path = "hello.html")]
struct HelloTemplate<'a> {
    name: &'a str,
}

This struct implements the Template trait, which can be derived automatically. Using the template attribute, we specify the path to the HTML template connected with this struct. The Rust compiler will generate Rust code from the templates, which is compiled into your application’s crate.

In a Rocket application, you can render HTML using this template by simply returning an instance of HelloTemplate:

#[get("/hello/<name>")]
fn hello<'a>(name: &'a str) -> HelloTemplate<'a> {
    HelloTemplate { name }
}

This approach allows us to verify at compile-time that the templates are syntactically correct, that they only make use of variables we actually pass to the template, and that they have the correct type.

For many errors, you will get instant feedback in your IDE or editor, for example if you refer to an HTML template file that does not exist, or if the fields of the struct don’t match with the variables used in the template. In the latter case, however, you will only see errors when editing the Rust source file containing the annotated template struct, not while editing the HTML template.

Since Jinja2 does not have a notion of typed partials, Askama only supports verification of the contract between your view model and the corresponding template. Verifying that partials or components are used correctly in your templates is not possible.

Hot reloading works using Cargo Watch. With cargo watch -x run, you can have cargo re-build and re-run your application whenever any source file changes, whether it’s a Rust source file or an HTML template. If all you do is change a single template, it usually takes a few seconds to restart your application.

Maud

All of the template engines we have seen so far make use of external HTML template files. For a completely different approach, we’re going to have a look at Maud, which uses an internal DSL to generate HTML instead.

Maud represents generated HTML with the Markup type. To generate a value of type Markup, you use its html! macro. This macro expects a single argument: a template using Maud’s syntax. In a Maud template, you can write arbitrary tag names and attributes, so that there is no problem if you need to use custom elements or data attributes, for example. Here is our example using Rocket and Maud:

#[get("/hello/<name>")]
fn hello_world(name: &str) -> Markup {
    html! {
        html {
            head {
                title { "Hello world!" }
            }
            body {
                h1 { "Hello, " (name) "!" }
            }
        }
    }
}

The benefits of Maud are similar to those of Askama: It’s impossible to refer to variables that don’t exist in your template, for example, and deployment is easy because all HTML templates are compiled into your crate. Hot reloading works the same — your application can be recompiled and restarted on every change using Cargo Watch.

Whereas Askama lets you write HTML sprinkled with some additional syntax for variables, conditions, and loops, Maud’s syntax is less similar to the generated output. This may or may not be a drawback for you.

One advantage is that it is less likely to write invalid HTML, because closing tags are generated for you. Due to the fact that Maud templates are not defined in external files, but in ordinary Rust functions, you also get immediate feedback about errors as well as autocompletion in your Maud templates.

More importantly, you can use Rust’s built-in features for defining and composing reusable elements, in this case, reusable snippets of Markup: functions. There is no need to add explicit support for layouts, template inheritance, or partials. This means that you can define re-usable HTML components as ordinary Rust functions, possibly in a separately released crate that can be pulled in by all teams.

Verdict

There are a few template engines in the Java ecosystem that use statically-typed templates. Most of them generate Java or Scala source files from external templates, and those generated sources are then be picked up by the compiler.

So what are the benefits you get from these templates engines? Since ordinary Java or, in one case, Scala sources, are generated from your templates, your IDE can help you with autocompletion and compile error messages when it comes to passing variables to your template and using it to to render HTML. These are errors you would otherwise only notice by manually interacting with your running application in the browser, or by writing corresponding tests.

However, IDEs often have problems picking up changes in your HTML templates and triggering a new source generation and compilation, so some external tooling is necessary for these benefits to take effect.

Twirl also gives you error messages and autocompletion when editing template files. However, this is only achieved by means of an IDE plugin that knows Twirl, it doesn’t come automatically just because Twirl is type-safe and generates Scala source files at build-time.

A component-based approach to HTML templating is only supported by Twirl and Rocker. These two verify at compile-time that your templates use components correctly, and it would be straightforward to maintain a Twirl or Rocker component library that can be pulled in as a dependency by all teams. However, of these two, only Twirl has a proper IDE support, assisting you with code completion when using these components in your templates.

When editing templates, many developers want to make use of hot reloading in order to get quick visual feedback in the browser. This is very easy for template engines that parse templates at runtime. For most of the template engines in the Java ecosystem we have examined in this post, hot reloading is an issue: While Twirl with Play supports hot reloading, the Scala compiler is notoriously slow. If you have complex templates with various included partials and layouts, it can take quite a while until you finally see your changes in the browser. With others, like Rocker or JStachio, hot reloading doesn’t seem to work or is not even advertised as a feature.

Depending on how important this instant visual feedback is to you, the benefits of type-safe templates using code generation may not be worth the price for you. If it’s important to you, Qute offers a fast hot reloading feature when used with Quarkus, though.

Apart from avoiding errors at build-time, generating source code from templates also often comes with performance benefits compared with templates that are parsed at runtime. Apart from Rocker, all the template engines also avoid reflection internally, which gives you additional performance benefits — though whether you need them or not is highly dependent on the quality requirements of your application. Avoiding reflection is more important because it makes it straightforward to use these template engines with GraalVM. Again, whether that’s relevant at all is a question that you and your stakeholders need to answer.

All in all, the Java template engines we looked at suffer from one of two problems: mediocre IDE integration or slow compilation times. Only Qute offers a real alternative if getting your build-time feedback in your browser is something that you’re fine with. It gives you fast, meaningful feedback, just not in your IDE, and it does not make the mistake of JSP to allow you to embed arbitrary Java code in your templates. However, if you intend to use a type-safe component library, Qute’s build-time verification capabilities are not good enough, as they only cover the interface between your view model and the template.

So are the Rust alternatives any better? Unlike some of the Java template engines we looked at, the two Rust alternatives both have a working solution for hot reloading. It’s not as fast as Qute with Quarkus, but faster than Twirl with Play. So if it’s important to you to get relatively fast visual feedback, both Askama and Maud have an acceptable solution.

Both alternatives can also give you quick feedback about errors as well as autocompletion when calling your template. Maud is the only template engine apart from Twirl that gives you autocompletion and instant error messages in your IDE. However, it does not rely on any special solution in terms of an IDE plugin. Since its templates are actually ordinary Rust functions, it simply uses the Rust compiler and the Rust language server. There is no special treatment compared to any other Rust code. Since all markup is generated by ordinary Rust functions, Maud also has a built-in solution for statically-typed HTML components.

If having templates embedded in your application code sounds attractive to you, this is something that is, today, not provided by any actively maintained library. There are libraries that are very similar to Maud on the JVM, for example Kotlin’s typesafe HTML DSL, or Clojure’s Hiccup. An experimental Java port of Hiccup hasn’t actively been maintained for seven years, but shows that, in principle, this approach would work in Java as well.

If you are willing to pull in or move to another JVM language, Hiccup or Kotlin’s DSL might be worth looking at. That being said, the latter might be a bit too type-safe. While Maud allows writing custom elements and arbitrary data attributes, this does not seem to be possible with Kotlin’s HTML DSL.

In the upcoming third and final part of this blog series, we are going to examine the topic of SQL queries.