Prost and Contrast of Quarkus, Qute with HTMX

19 Feb, 2025

Prost and Contrast of Quarkus, Qute with HTMX

Quarkus comes with its own powerful template engine called Qute as part of its ecosystem. Qute's responsibility is straightforward: it takes data from the server, binds it to a template, and produces HTML. Thanks to its fragment structure, you can render only the relevant part of the page rather than the entire page — and that is a significant flexibility. However, Qute's responsibility ends on the server. Where to place the produced fragment on the page, how to combine data from multiple forms in a single request, how to update the URL; Qute cannot handle any of these, because they are client-side concerns. HTMX steps in exactly at this point, managing these decisions through HTML attributes without writing a single line of JavaScript. Shortly, Qute produces on the server; HTMX places it on the client. Together, you need neither the complexity of a SPA (Single Page Application) nor the clunkiness of a full page reload.

What Can We Build with Qute + HTMX?

To make this concrete, let's walk through an e-commerce scenario: a product listing page where users can search for computers and filter results by brand and model.

1. Page Structure — The Fragment Concept

With Quarkus Qute, we can render specific HTML pages on the server side. Each of these HTML files is a template for Qute. Instead of writing one large HTML file, it is possible to break the page into parts based on their responsibilities. These independent HTML pieces within the main page skeleton are what we call fragments.

product/
├── product.html           ← Main page skeleton
└── fragments/
    ├── search-bar.html    ← Search form
    ├── results-area.html  ← Product list (HTMX's target)
    └── product-card.html  ← Single product card

2. Main Page - Including Fragments

Inside product.html, we pull the search form and results area from separate template files:

<!DOCTYPE html>
<html>
<body>
    <div class="container">
        <h1>Search</h1>

        <!-- Search form as a separate fragment -->
        {#include "product/fragments/search-bar.html" /}

        <!-- Results area as a separate fragment -->
        {#include "product/fragments/results-area.html" /}
    </div>
</body>
</html>

The page skeleton stays clean, and each piece lives in its own file. Qute merges these includes on the server and returns them as a single HTML document.

3. Search Form Fragment

fragments/search-bar.html:

<div id="searchBar">
    <form id="searchForm"
          hx-get="/products"
          hx-target="#resultsArea"
          hx-swap="outerHTML"
          hx-push-url="true"
          hx-trigger="submit">

        <input type="text"
               name="q"
               value="{params.get('q') ?: ''}"
               placeholder="Search computers...">

        <select name="brand">
            <option value="">All Brands</option>
            <option value="apple"
                    {#if params.get('brand') == 'apple'}selected{/if}>
                Apple
            </option>
            <option value="dell"
                    {#if params.get('brand') == 'dell'}selected{/if}>
                Dell
            </option>
            <option value="lenovo"
                    {#if params.get('brand') == 'lenovo'}selected{/if}>
                Lenovo
            </option>
        </select>

        <select name="model">
            <option value="">All Models</option>
            <option value="gaming"
                    {#if params.get('model') == 'gaming'}selected{/if}>
                Gaming
            </option>
            <option value="ultrabook"
                    {#if params.get('model') == 'ultrabook'}selected{/if}>
                Ultrabook
            </option>
        </select>

        <button type="submit">Search</button>
    </form>
</div>

Qute's {params.get('q') ?: ''} syntax is worth highlighting here — after a user performs a search, the form fields come back pre-selected with the values returned from the server. No extra JavaScript required.

4. Results Area Fragment

fragments/results-area.html:

<div id="resultsArea">
    {#if res.items.isEmpty()}
        <p>No results found.</p>
    {#else}
        <p>{res.total} products found</p>
        <div class="product-grid">
            {#for product in res.items}
                {#include "product/fragments/product-card.html"
                    product=product /}
            {/for}
        </div>

        <!-- Pagination -->
        <div class="pagination">
            {#if prevUrl}
                <a href="{prevUrl}">← Previous</a>
            {/if}
            {#if nextUrl}
                <a href="{nextUrl}">Next →</a>
            {/if}
        </div>
    {/if}
</div>

We call product-card.html in a loop from within results-area.html. Each fragment carries its own responsibility — if the card design changes, you only need to touch product-card.html.

5. Java Endpoint — Returning the Right Template

@Path("/products")
public class ProductTemplate {

    @Inject
    @Location("product/product.html")
    Template productPage;

    @Inject
    @Location("product/fragments/results-area.html")
    Template resultsArea;

    @Inject
    ProductService productService;

    @GET
    @Produces(MediaType.TEXT_HTML)
    @Blocking
    public TemplateInstance get(
            @QueryParam("q") String q,
            @QueryParam("brand") String brand,
            @QueryParam("model") String model,
            @QueryParam("page") @DefaultValue("0") int page,
            @QueryParam("size") @DefaultValue("12") int size,
            @HeaderParam("HX-Request") String hxRequest,
            @Context UriInfo uriInfo
    ) {
        PageResponse<Product> res = productService.search(q, brand, model, page, size);

        int totalPages = Math.max(
            (int) Math.ceil((res.total * 1.0) / Math.max(size, 1)), 1
        );

        Map<String, String> params = new LinkedHashMap<>();
        uriInfo.getQueryParameters()
               .forEach((k, v) -> { if (!v.isEmpty()) params.put(k, v.get(0)); });

        String prevUrl = page > 0
            ? buildUrl("/products", with(params, "page", String.valueOf(page - 1)))
            : null;
        String nextUrl = (page + 1) < totalPages
            ? buildUrl("/products", with(params, "page", String.valueOf(page + 1)))
            : null;

        // Is it an HTMX request? Return only the fragment.
        boolean isHtmx = hxRequest != null && hxRequest.equalsIgnoreCase("true");
        Template template = isHtmx ? resultsArea : productPage;

        return template
                .data("res", res)
                .data("params", params)
                .data("page", page)
                .data("totalPages", totalPages)
                .data("prevUrl", prevUrl)
                .data("nextUrl", nextUrl);
    }
}

Here is how the flow works:

💡 User visits the page
→ Normal request
product.html is returned (full page)
→ Includes are merged by Qute

User searches for "Apple Gaming"
→ HTMX intercepts the form submission
→ Sends /products?brand=apple&model=gaming with HX-Request: true header
→ Server detects isHtmx
→ returns only results-area.html
→ HTMX updates #resultsArea
→ Form fields remain pre-selected via param.

Benefits

The first thing we noticed when using Qute and HTMX together is that no knowledge of JavaScript frameworks is required. Instead of dealing with the learning curve of React or Vue, as a backend developer, you can pick up right where you left off and build dynamic user interfaces using the Java and HTML you already know. Returning HTML directly from the backend reduces the number of layers; since there’s no pipeline that generates JSON and then converts it back to HTML on the frontend, the number of potential points of error naturally decreases.

When it comes to fragment rendering, HTMX’s contribution is undeniable. Only the part of the page that needs to be updated is requested from the server and refreshed, meaning there is no full page reload and the user experience remains seamless. In addition, Qute’s server-side rendering offers a significant advantage for SEO. When crawlers access the page, the content is already ready, so they don’t have to wait for JavaScript to execute.

Another major strength of Quarkus is that it features a type-safe template engine. If you use an incorrect variable name in your templates or encounter a type mismatch, you’ll discover this at compile-time rather than runtime. This feature alone may be reason enough to choose Quarkus over alternatives like Thymeleaf or Freemarker.

Specifically regarding Quarkus, thanks to native build support, your application runs with an incredibly low memory footprint. This translates directly into cost savings in containerized environments and cloud costs. During the development process, the hot reload feature provided by dev mode instantly reflects changes to your templates, eliminating the need to restart the application.

Disadvantages

You typically first encounter the limitations of the Qute and HTMX combination in scenarios that require complex client-side interactivity. For scenarios such as drag-and-drop interfaces, real-time dashboards, or multi-step wizard forms, HTMX’s attribute-based approach begins to fall short. At this point, you inevitably have to write JavaScript or rely on a frontend library.

Fragment management also brings its own complexity as a project grows. While managing two or three fragments on a small page feels quite straightforward, dozens of fragments, nested includes, and interdependent HTMX requests can eventually turn into a structure that’s difficult to keep track of. It becomes harder to keep track of which fragment is rendered where and what each endpoint returns.

From an ecosystem perspective, Qute’s most significant drawback is the small size of its community. When you encounter an issue, you may not always be able to find someone who has experienced the same problem on Stack Overflow or in GitHub issues. But at Plaincoder, we’re delighted to serve as a resource for you through the projects we’ve developed using Qute and HTMX!

Conclusion

As Plaincoder, we’ve opted for the Quarkus + Qute + HTMX trio, particularly for our B2B projects. The primary motivation behind this choice was speed and simplicity. When we wanted to develop user-friendly interfaces where design takes center stage without relying on any frontend framework, this trio became the ideal combination for us.

Given the complexity of DOM management and the performance overhead that JavaScript brings to modern web projects, Qute’s server-side rendering approach became an inevitable choice for us. Less JavaScript, fewer layers, fewer things that can go wrong.

We acknowledge that our community is still small, but that hasn’t stopped us. Thanks to the best practices we’ve established throughout our projects, we’ve been able to take full advantage of Qute without getting bogged down in a complex architecture. We’ll cover in detail how we developed these best practices and scaled our fragment management in a future post.

Launch Your Next Big Idea With Us

Let’s Talk!