REST API using Quarkus and Panache

Manollo Guedes
11 min readFeb 16, 2022

--

Quarkus REST API

You can find the Portuguese version of this article at this link.

Quarkus is a Java-based framework that brings in its essence the idea of improving resource utilization. For this, Quarkus applications are optimized especially for serverless, containerized, and cloud environments.

This article intends to be a quick guide for a first REST API using Quarkus and Panache, besides some other helpful Java libs.

Enjoy your journey 🚀

What are we planning to code here?

This project should be basically an API to manage products.

After working on this simple and straight project, we'll learn how to:

  • generate projects using Quarkus UI
  • build a REST API using RESTEasy
  • persist and manage data using Hibernate and Panache

Also, by the end of this article as a bonus, we'll talk about the Active Record pattern and how to use Panache to develop following this approach.

You can find the final version of our project on this GitHub repository.

Generating the Project

Following Spring initializer's suit, Quarkus provides us with an amazing tool for bootstrapping our applications. You can access the tool to generate your project on this link.

Screenshot from the Quarkus tools to generate applications
Platform to generate projects using Quarkus

Using this tool, you can add as many Quarkus extensions as your project demands.

For this project, we are going to use only a few.

Here is the list of dependencies we must have. In the following sections of this article we'll dig in on each one of these:

  • quarkus-resteasy-jackson
  • quarkus-hibernate-orm-panache
  • quarkus-jdbc-h2

After adding these extensions, generate your application and import it into your favorite IDE (I'm using IntelliJ).

Creating our model

The only resource we will have on this project are products.

So, to begin our development let's create a new package called org.acme.model. And inside this, add the class Product with the following code:

Product model

This class has a couple of annotations that maybe you are wondering where are they from.

Besides the extensions we previously added to our project, I’m using a Java library called Lombok. Using this lib we can simplify our development, saving us to create a lot of methods like getters, setters, constructors, and a lot more.

To add this lib into our project, we need to add this dependency into our pom.xml:

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>

Bellow, you can find a quick explanation about the annotations we are using in the Product class, but you can check on all Lombok's features accessing the lib's website.

  • Data: generates getters, setters, toString, equals, and hashCode methods. Besides generating a constructor with all required properties (final properties).
  • Builder: generates all the code we need to follow the builder pattern. Having the property toBuilder set as true, we can generate a builder from an already built object by using the method toBuilder().
  • AllArgsConstructor and NoArgsConstructor: both annotations tell Lombok to generate constructors. The first annotation generates a constructor to initialize all attributes on the class, and the second annotation generates the default empty constructor.

Note: Some IDEs request us to install its Lombok plugin to work properly.

Creating the API endpoints

To manipulate our Product resource, we'll create a new controller to manage our API endpoints.

To do that we need to create either a new package org.acme.controller, and a new class ProductController in it.

With this new class we want to be able to have a CRUD (create, read, update and delete) of products. So for each one of these operations, we'll have an endpoint.

The first version of our ProductController code might be something like this:

ProductController

Note that we are only using fake data for now. On the next steps we'll work with real data, so hold on! 😃

To work with REST API, Quarkus uses the RESTEasy implementation of the JAX-RS specification.

Previously we have added to our project the extension quarkus-resteasy-jackson. This extension gives our project the ability to create a REST API, and besides that, provides to our REST API the power of process data (incoming and outgoing) using the lib Jackson.

— Ok, stop talking about the tools we are using under the hood, let's get back to our code.

Since Quarkus follows the JAX-RS specification, we need to annotate our class with @Path to tell Java what is the path that it needs to listen to when we run our application. In this case, our controller is listening to the path “/api/v1/product”.

We are also annotating our controller with @Produces and @Consumes. What does it mean? It explicitly defines what our class expects to receive from a request and what it is going to produce as a response. We can annotate either classes or methods with both annotations.

But we are using quarkus-resteasy-jackson! Due to that Quarkus will expect to consume and produce JSON data by default. So, no need to add neither @Produces nor @Consumes annotation. We are adding it here only because it's a good practice.

Now we have a runnable REST API ready to receive its first test.

To run our application we can use the following command:

mvn quarkus:dev

And hit the GET endpoint, for example, using this command:

curl --location --request GET 'http://localhost:8080/api/v1/product'

Using Panache to persist data

— Ok, so far we have used only mocked data. It's time to start coming to the real scenario when we really keep and manage some data for real.

Quarkus uses Hibernate ORM as the JPA implementation. The panache extension we added before (quarkus-hibernate-orm-panache) is focused on turning our entity management even easier for us.

Using Hibernate we don't need to manually create our tables or define our constraints. We can use classes called entities to do that. Our Product model can be turned into exactly what an entity is: a definition of a database table. Let's do it! 👏

Configuring our Project to connect to a Database

You have noticed that when we first run our application we asked maven to run Quarkus using the flag dev:

mvn quarkus:dev

It means we are starting our application in dev mode.

Quarkus provides an easy way to go when we run it locally in dev mode. One of the benefits of this feature is the ability to code without worrying about DB configurations.

Yes, you read it correctly!

If you are starting your project working with Quarkus, you don’t need to worry about DB configs, Quakus can do it for you by itself. The only thing you need to do is do not provide any database URL, username, and password onto your application.properties.

Quarkus supports a list of open source databases, you can check it on Quarkus Datasource Documentation. In this article, we are going to work with H2.

Quick note: If you want to work with any other database instead of H2, you’ll need to start a Docker daemon first.

To work with H2 we added the quarkus-jdbc-h2 extension.

After doing that, the only thing we need to do to complete our DB config to use dev-mode is to specify which kind of data source our project is going to use. This configuration must go on our application.properties file.

quarkus.datasource.db-kind=h2

Having this single configuration done, we can now start working on our code. As simple as that…

Turning the class Product into an Entity

First of all, we need to tell Hibernate that our Product class is an Entity.

How?

We just need to annotate the Product class as @Entity, it will both turn this class usable in queries and use this class to define our table. If you want to define some specific table details such as name, schema, and so on, you can use the annotation @Table. In our example, I'm changing the table's name from Product (capitalized) to product (lower case).

Every table has a primary key. To do that we need to annotate our id property with @Id. If we want to use pre-built hibernate sequences to generate each id, we also need to annotate the property with @GeneratedValue. Otherwise, we'll need to generate the id on the Java side.

We also have the @Column annotation. It gives us the ability to define other details around each column, such as nullability, precision, length, and so on.

The final version of our Product class might be something like this:

Product class with entity related annotations

Creating a Repository using Panache

As mentioned before, Panache has a focus on turning our entity management easier. To do that, it gives us a utility belt having a lot of methods to manipulate our entities, like findById, list, update, persist and delete.

To be able to access these methods we need to have a new class called ProductRepository that implements PanacheRepository<Product>. This new class must be added to a new package org.acme.repository.

The first looking of our class might be something like this:

ProductRepository

With this simple code, we are already able to use every method provided by Panache. We just need to import this new class and start using it.

But it's not enough for our scenario…

When we defined our endpoints on the ProductController you must have noticed that we have, for example, a GET method that can receive either a product and/or a brand name to look for products.

How can we do that using only the default methods?

Answer: We can't!

Panache has just standard methods that are really handy for us, such findById, but sometimes they are not enough to cover every sort of scenario we have. Then we need to create more methods to manipulate our data.

So, let's start creating 3 new methods on our ProductRepository class: findByName, findByBrand, and findByNameAndBrand.

ProductRepository findBy methods

Having these methods we are able to use our GETs endpoints in their entirety.

But, when we are looking for a product by its name or brand we should be able to find it without worrying about if our search mechanism is or is not case sensitive. To do that we can either pre-format every single data we are saving into our database or convert the data at the exact moment we are performing a search.

Both options are easy to implement, but the second one can negatively affect the database performance, so let's use the first one.

To do that we need to create our own persist method that receives a Product and capitalize each word in its name and brand name.

Let's start adding a new dependency, apache commons-text to our pom.xml, this dependency gives us some utility methods to work with text.

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.9</version>
</dependency>

Our persist method might look like this:

ProductRepository persistence method overload

You can see that we are not only overloading the method persist, but we are also using the implementation provided by the PanacheRepository to save the data after normalizing it. By doing this we guarantee that no matter who is using our repository, every single product being persisted will have the same name and brand formatting.

Last but not least, we need to implement the method update to guarantee that we are looking for a product having the intended id and will replace its information.

ProductRepository method to update products

Panache has a pre-built update method, but it uses a query to set values, so it's easier to have a new method that does it using Java instead of HQL or JPQL.

Ok, having done everything related to data manipulation, we now need to make a choice between having a Service Layer or using our Repository directly on our Controller. I like to have a Service Layer to control validations and other business logic, if you don't want to use it you can start using your repository right now. Otherwise, let's create our Service.

Service layer for incoming data validation and business logic

Our new class ProductService will be placed into a new package called org.acme.service. And we will use it to access our repository and manipulate data.

To do that we need to inject our repository bean into our class.

— How to do that? 🤔

That is simple!

Quarkus uses CDI (Context and Dependency Injection). Long story short, Quarkus is responsible for the beans discovering and injection into both classes and contexts that may use these beans.

Quick note: CDI is a really complex subject. If you want to read more about it to understand it deeper, you can check on the Quarkus documentation about CDI and also on the Jakarta documentation for CDI using Java.

Getting back to our project, if you check again on our ProductRepository you will notice that we have annotated our class as @ApplicationScoped. It marks our class as a bean that has its scope as the application lifecycle. This way, every constructor that demands a ProductRepository instance as an argument can take advantage of the ProductRepository injection. Turning unnecessary to create and/or control the lifecycle of the repository, as Quarkus will take care of that for us.

— Alright, so it means that we'll need to have a constructor for our ProductService class, right?

Exactly! But we don't need to manually create it, we have Lombok to do that for us 👏. We just need to annotate ProductService as @AllArgsConstructor. It is enough to ask Lombok to create a constructor that receives all class' attributes as params. So, this way, our ProductService is going to receive the ProductRepository bean. 🕺

This is the final version of our ProductService:

ProductService

In this class, we have every method that we need to use on ProductController.

As you can see, the methods create, replace, and delete are annotated as @Transactional.

— What does it mean?

When we have methods that modify our database we need either control their transactions manually or ask Hibernates to take care of this for us by annotating either the entire endpoint, class, or method with @Transactional. By doing that, after the method execution, Hibernate will either commit or do a rollback depending on the execution status.

Updating the ProductController to call the Service instead of using fake data

Now we can update our controller to call our service to manipulate our data instead of responding to our requests with mocked data.

The final version of our ProductController might be something like that:

By this point, you have everything your code needs to work and store data.

If you have any problem you can check on the final code we have developed together so far!

Final Bonus: Active Record Pattern

Active Record is a pattern used to give more powers to our Entity class. Following this pattern, we basically don't need to have a Repository class, the Entity itself is responsible to carry both data and behavior.

Martin Fowler describes an Entity that follows this pattern as:

An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.

To dismiss the need of having another class accessing the database, Active Record Entity must provide methods to access and manipulate its data.

Using Panache we can easily do everything we need to turn our ProductEntity into an ActiveRecord. What we need to do is just extend the PanacheEntity class. By doing that, we no longer need to have our ProductRepository.

Product class using Active Record Pattern

Now our Product class is not only able to represent the product data but also manipulate it using the same default method we once had on the ProductRepository, like findById, persist, and all the others.

But as Uncle Ben said:

With great power comes great responsibility

Active Records allows us to access data using our domain class, but it brings with us a couple of disadvantages:

  • High coupling: Active Records mixes the persistence and domain layers turning them really tied to each other.
  • Create tests becomes harder: as we have a strong coupling between persistence and domain layers, it's really hard to mock one layer and properly test the other.
  • SRP: The Single Responsibility Principle cries when we talk about Active Records to it. Our classes can easily become a monster having dozens of methods.

So, as Software Developers, we need to evaluate the pros and cons before each decision. To use Active Records or not wouldn't be different.

--

--