In this blog post I’ll show you how develop a Quarkus application to easily access your relational database (using Hibernate ORM with Panache) and display its content in HTML with Qute templates. To break it into more details, in this blog post you will learn:
- What is Hibernate ORM with Panache?
- How to access a relational database with Hibernate ORM with Panache.
- What are Qute templates?
- How to display the data from the database in HTML using Qute templates.
- Beautify the Qute templates with Tweeter Bootstrap.
Use Case
Let’s say you have a relational database storing a list of books, and your users want a visual interface to query the list of books as well as see the details of a book. There are several tools you can choose from… but have you thought of developing it with Quarkus? Quarkus allows you to easily access relational databases, thanks to Hibernate ORM with Panache, as well as using templating thanks to Qute.
The diagram below shows the overall architecture of what will be detailed in this blog post:
- The
Book
entity is a Panache entity mapped to a PostgreSQL database (with aBook
table). - The
BookPage
is a Qute resource (a JAX-RS endpoint with some Qute specific APIs) that uses two templates (book.html
andbooks.html
) to display the list of books and the detail of one book. base.html
is a base Qute template, included in the two others, where we’ll add Tweeter Bootstrap.

Accessing the Database with Hibernate ORM with Panache
In Java, we have several APIs and frameworks to map objects to relational databases. One of these famous frameworks is Hibernate which implements the JPA specification. Hibernate makes mapping and querying Java objects easy. Hibernate ORM with Panache is built on top of Hibernate, and makes simple mapping and simple queries trivial.
Panache Entity
To store books into a relational database we use a Book
Panache entity. Like a standard JPA entity, Book
is annotated with @Entity
and can use other JPA annotations (eg. @Table
, @Column
, etc.). One difference though, is that Panache entities extend either PanacheEntity
(and therefore get an identifier) or PanacheBaseEntity
(and therefore manage their own identifier).

The code below shows the Book
Panache entity. As you can see, there is no @Id
annotation for the identifier because it inherits it from the super class PanacheEntity
. It is annotated with @Entity
and uses @Table
to specify the name of the table where to map books, as well as @Column
to map to specific columns. One difference with straight JPA entities, is that all attributes can be public, and getters and setters become optional.
@Entity
@Table(name = "t_books")
public class Book extends PanacheEntity {
@Column(length = 100, nullable = false)
public String title;
@Column(length = 20)
public String isbn;
@Column(nullable = false)
public BigDecimal price;
// ....
Active Record Pattern
By inheriting from PanacheBaseEntity
, our Book
entity benefits from many methods allowing CRUD operations and queries. This is known as the Active Record Pattern. This allows you to do the following:
Book.findById(id);
Book.findByIdOptional(id);
Book.deleteById(id);
Book.deleteAll();
Book.count();
That’s for CRUD operations, but PanacheBaseEntity
also has a set of methods to query entities, sort and paginate them:
Book.list(query);
Book.find(query).list();
Book.find(query, Sort.by(sort)).list();
Book.find(query, Sort.by(sort)).page(pageIndex, pageSize).list();
Hibernate ORM with Panache also simplifies the Java Persistence Query Language (JPQL) defined in JPA. It allows you to write queries without the SELECT
and FROM
clause. You only concentrate on the WHERE
clause (without having to use the WHERE
keyword):
Book.list("price < 10", Sort.by("isbn"));
Book.list("price < 10 and nbOfPages > 100");
Book.list("price < 10 and nbOfPages > 100", Sort.by("isbn"));
Book.find("price < 10 and nbOfPages > 100", Sort.by("isbn")).list();
Book.find("price < 10 and nbOfPages > 100", Sort.by("isbn")).page(2, 4).list();
Visualising Data with Qute
Now that we have a Book
entity with methods to query it, let’s add a few Qute resources to visualise the list of books and the details of a book. Qute is yet another templating engine that fits well with Quarkus. The Quarkus documentation specifies that:
Qute is a templating engine designed specifically to meet the Quarkus needs. The usage of reflection is minimized to reduce the size of native images. The API combines both the imperative and the non-blocking reactive style of coding.
Declaring the Qute Templates
There are different ways to declare Qute templates, but I quite like the type safe way. The idea is to create a Qute resource (here called BookPage
) and define the templates with code. In the example below, we define two templates called book()
and books()
.
Notice that these methods can take parameters. The book()
method takes a book
object so it can access the attributes of a book
and display them in HTML. As for books()
, it takes a list of books that the template will iterate through and display.
@Path("/page/books")
@Produces(MediaType.TEXT_HTML)
@ApplicationScoped
public class BookPage {
@CheckedTemplate
public static class Templates {
public static native TemplateInstance book(Book book);
public static native TemplateInstance books(List<Book> books);
}
// ...
The type-safe approach relies on some conventions. The Qute templates must have the same name as defined in the code (eg. book()
for book.html
). Then, they must be located under the /src/main/resources/templates
directory, under a sub-directory named after the Qute resource (here BookPage
).

Displaying the Book Details
To display the details of a book, we now need to create a method that accesses the database giving a book identifier. In the BookPage
resource, notice the showBookById()
method. It uses JAX-RS annotations (@GET
, @Path
, @PathParam("id")
) so it can handle an HTTP request such as http://localhost:8080/page/books/2. Notice how Book.findById(id)
uses the Active Record pattern to get the Book
entity by its identifier.
@Path("/page/books")
@Produces(MediaType.TEXT_HTML)
@ApplicationScoped
public class BookPage {
@CheckedTemplate
public static class Templates {
public static native TemplateInstance book(Book book);
}
@GET
@Path("/{id}")
public TemplateInstance showBookById(@PathParam("id") Long id) {
return Templates.book(Book.findById(id));
}
// ...
Once the book is found, it is returned withing the book()
template. The Qute engine will then look for the template under src/main/resources/templates/BookPage/book.html
and pass the book
object as a parameter. The book.html
file below is quite simple. it uses an expression language to access the attribute of the book
object: {book.id}
accesses the id
attribute of the book
object that was passed in Templates.book(Book.findById(id))
.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Book</title>
</head>
<body>
Id: {book.id}
Title: {book.title}
Description: {book.description}
Price: {book.price}
Isbn: {book.isbn}
Number of Pages: {book.nbOfPages}
Publication Date: {book.publicationDate}
Created Date: {book.createdDate}
</body>
</html>
Displaying the List of Books
To display the list of books, we need to query them. And that’s when Panache makes life easy. The showAllBooks()
method takes the needed parameters to execute a query with sort and pagination. So, for example, if you invoke the showAllBooks()
with the following HTTP request:
http://localhost:8080/page/books?query=price < 50 and nbOfPages > 100 &sort=isbn&page=1&size=5
It will execute the following Panache query:
Book.find("price < 50 and nbOfPages > 100", Sort.by("isbn")).page(1, 5))
@Path("/page/books")
@Produces(MediaType.TEXT_HTML)
@ApplicationScoped
public class BookPage {
@CheckedTemplate
public static class Templates {
public static native TemplateInstance books(List<Book> books);
}
@GET
public TemplateInstance showAllBooks(
@QueryParam("query") String query,
@QueryParam("sort") @DefaultValue("id") String sort,
@QueryParam("page") @DefaultValue("0") Integer pageIndex,
@QueryParam("size") @DefaultValue("1000") Integer pageSize) {
return Templates.books(Book.find(query, Sort.by(sort)).page(pageIndex, pageSize).list())
.data("query", query)
.data("sort", sort)
.data("pageIndex", pageIndex)
.data("pageSize", pageSize);
}
// ...
Notice that when we invoke the books()
template, we pass the list of books returned by the query, but we also add the parameters to the template (eg. data("sort", sort)
). This is another way to pass data to the template.
The template loops through the list of books ({#for book in books}
) and displays the attributes ({book.isbn}
, {book.id}
). To display the data that was passed to the template (data("sort", sort)
) we use the special data
namespace ({data:sort}
).
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Books</title>
</head>
<body>
<code>Book.find({data:query}, Sort.by({data:sort})).page({data:pageIndex}, {data:pageSize}).list()</code>
<table>
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Title</th>
<th scope="col">Isbn</th>
<th scope="col">Price</th>
<th scope="col">n° Pages</th>
<th scope="col">Publication Date</th>
</tr>
</thead>
<tbody>
{#for book in books}
<tr>
<th scope="row"><a href="http://localhost:8080/page/books/{book.id}">{book.id}</a></th>
<td>{book.title}</td>
<td>{book.isbn}</td>
<td>{book.price}</td>
<td>{book.nbOfPages}</td>
<td>{book.publicationDate}</td>
</tr>
{/for}
</tbody>
</table>
</body>
</html>
Beautifying the Templates
To beautify these two templates, we can use a base template that both will include. That’s the perfect location to add some Tweeter Bootstrap (CSS and even JavaScript if needed). That’s what the base.html
does. It also uses an #insert
section used to specify parts that could be overridden by the child templates: here, the title ({#insert title}
) and the body ({#insert body}
) of the included template:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous">
<title>{#insert title}Default Title{/}</title>
</head>
<body>
<div class="container">
<h1>{#insert title}Default Title{/}</h1>
{#insert body}No body!{/}
</div>
</body>
</html>
To include the base.html
template in the book.html
and books.html
templates, it is just a matter of including it ({#include base.html}
) and overriding the specific sections ({#title}{/title}
and {#body}{/body}
).
{#include base.html}
{#title}{books.size} Books{/title}
{#body}
<!-- body -->
{/body}
{/include}
Executing the Application
To execute the code, you need Docker to be up and running. Why? Because the data is stored into a PostgreSQL database and we use Quarkus DevServices to automatically start it. The way DevServices works, is that it detects that the application needs a PostgreSQL database (because it is declared as a dependency in the pom.xml
) and uses TestContainers behind the scenes, to download the Docker image, start and stop it.
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
This happens just by starting Quarkus with the following command:
$ mvn quarkus:dev
Then, you can point your browser to the following URLs so you can query the database and display different lists of books:
http://localhost:8080/page/books?query=price < 10
http://localhost:8080/page/books?query=price < 10 and nbOfPages > 100 &sort=isbn
http://localhost:8080/page/books?query=price < 50 and nbOfPages > 100 &sort=isbn&page=1&size=5
http://localhost:8080/page/books?query=price < 50 and nbOfPages > 100 &sort=isbn&page=2&size=5

To get the details of a book, you can use the following URLs:
http://localhost:8080/page/book/1
http://localhost:8080/page/book/2

Conclusion
There are several ways to easily display data from the database. In this blog post I wanted to show you the combination of Qute and Hibernate ORM with Panache. Qute is a templating engine, so its main purpose is not to be yet another front-end framework. You can use Qute templates to write emails, send messages, etc. But writing HTML pages is also doable.
As for Hibernate ORM with Panache, it is a great way to make easy queries trivial. Remember that Hibernate ORM with Panache is built on top of JPA. So if you need the power of JPA, you can. By inheriting from PanacheEntity
you get access to the JPA EntityManager
and can use it whenever you want. You are not stuck to Hibernate ORM with Panache, you also have the full powser of JPA.
Now it’s your turn. Download the code and give it a try.
References
If you want to give this code a try, download it from GitHub, build it, run it, and make sure to break the communication between the microservices to see fallback in action.
You can get my books and on-line courses on Quarkus.

I lobe this approach as it is simple and super efficient. No JS/TS overhead.