- Part 1: Type-safe HTTP routing in Java and Rust
- Part 2: Type-safe HTML templates in Java and Rust (this article)
- Part 3: Type-safe SQL queries in Java and Rust
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:
- Parsed at runtime: Either when the application starts, or when a template is used for the first time, the template file is read from disk and parsed into an in-memory structure that allows for efficient dynamic rendering, taking into account the variable content passed to the template.
- Support for hot reloading in development: The template engine can be configured such that the parsed templates are not cached but rather loaded from disk every time they are used. Due to the performance penalty of doing so, this feature is typically only used during development, to enable what is known hot reloading or live reloading — as soon as you make a change in your template, that change will be visible in your locally running application.
- Error reporting in the browser: In development mode, changes are not only picked up immediately. In addition, if you introduce an error into your template, an error message will be shown in the browser.
-
Dynamically typed: The compile-time interface between the template and the code rendering it and passing data to it is completely untyped — typically something like a
Map<String, Object>
. Whether you pass variables with the correct name and type to the template, and whether the properties accessed by a template actually exist is not validated at compile time. Problems will only surface at runtime. Since the types of objects passed to a template are unknown at compile time, these template engines make use of reflection when templates reference properties of objects passed to them.
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
The template uses one variable, name
. The second element is a struct that has a corresponding field:
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
:
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:
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.