How to Build REST API and Test with Quarkus (Panache ORM, Repository Pattern & Rest-Assured)

Ichwan Sholihin
Stackademic
Published in
7 min readFeb 4, 2024

--

Photo by Riku Lu on Unsplash

Quarkus is an open-source Java framework that simplifies development for building applications with support for multiple programming languages, including Kotlin and Scala.

One of the interesting features of Quarkus is ability to run applications without explicitly requiring a main class. This can happen because Quarkus uses a technology called “Classpath scanning”. Typically, in a Java application, you have a main class that has a main method that is executed when the application starts. However, Quarkus uses a classpath scanning mechanism to find the beans or components that need to be loaded and run.

In this article, we will create a simple CRUD operation in Quarkus REST API up to unit testing.

Requirements:

  • JDK 17
  • Microsoft SQL Server/Jetbrains DataGrip/Azure Data Studio
  • IntelliJ IDEA

Starter Project

Similar to creating a project in Spring, Quarkus also provides project generation on the website https://code.quarkus.io/. Simply add the required libraries and then download the generated project in .zip format. In this Rest API project, we use several libraries such as:

  • RESTEasy Reactive dan RESTEasy Reactive Jackson: these two libraries implement the Jakarta REST API commonly used in Spring, allowing each entity created to be generated into a database table.
  • Hibernate ORM with Panache: this library will provide the PanacheRepository class for ORM implementation.
  • JDBC Driver-Microsoft SQL Server: we will use this driver to connect to the database.
  • Hibernate Validator: validates user input data.
  • Lombok (Optional): this library is not available on code.quarkus.io, you can add it yourself if you want to use it.

Once you have added these five libraries, download the generated project and open it in your preferred IDE. Let’s first connect the project to the MSSQL database. You don’t have to use the same database as in this tutorial; adjust it to the database you commonly use. Since I am using MSSQL, open the application.properties file in the resources directory and add the following configuration:

quarkus.datasource.db-kind=mssql
quarkus.datasource.username=sa
quarkus.datasource.password=Admin123
quarkus.datasource.jdbc.url=jdbc:sqlserver://localhost:1433;databaseName=quarkus;encrypt=true;trustServerCertificate=true;
quarkus.hibernate-orm.log.sql=true
quarkus.hibernate-orm.database.generation=create

Create the database first and then change the password and databaseName from the above configuration. If you are using PostgreSQL or H2 database, try reading the complete documentation on datasource:

Run the project using the command ./mvnw compile quarkus:dev if you are using the Maven build tool in the terminal.

Entity & DTO (Data Transfer Object)

Quarkus has two patterns in its Rest API specification, namely Active Record Pattern and Repository Pattern.

The Active Record Pattern allows us to create a combined class for both the entity and the read and write database operations, with the condition that the entity extends the PanacheEntity class, and all fields have public access modifiers, eliminating the need to add setters and getters. This makes it easy for us to monitor operations directly connected to the database without separating them. However, if we refer to one of the SOLID design patterns, namely the Single Responsibility Pattern, this contradicts it because it does not separate objects according to their responsibilities, especially for developers accustomed to using Spring who are used to creating a repository layer for database read-write operations.

In this article, we will use the repository pattern. First, we will create an entity class called Person.

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(schema = "dbo")
public class Person {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstname;
private String lastname;
private String address;
}

The @Column annotation is optional; there is no need to declare it if the variable name already represents the column name in the database table.

DTO or Data Transfer Object plays a crucial role in Rest API protocols, where data input and output to the user are handled in DTO. In accordance with the initial requirements, it is recommended to use Java 17 so that we can implement the record class feature. Unlike regular classes, record classes are immutable (data cannot be changed) and automatically generate default methods such as toString(), equals(), and hashCode(). Below is the DTO class code to store user requests.

import jakarta.validation.constraints.NotBlank;

public record PersonRequest(@NotBlank String firstname, @NotBlank String lastname, @NotBlank String address) {}

Add validator annotations to validate user input. In this article, I only created a DTO to handle requests, not for responses because the displayed data is the same as the entity. However, it is highly recommended for you to create a specific response DTO if the Rest API stores important data.

Repository

As discussed in the previous point, Quarkus has Active Record and Repository Pattern. If Active Record requires an entity to extend the PanacheEntity class, when using the Repository Pattern, a repository must be a class, not an interface, and that class should implement the PanacheRepository interface.

@ApplicationScoped
public class PersonRepository implements PanacheRepository<Person> {
}

The PanacheRepository interface is only available in the Hibernate ORM Panache library, which we installed at the beginning of the project. This interface is derived from PanacheRepositoryBase, which provides database operation methods.

Meanwhile, the @ApplicationScoped annotation is metadata that indicates the scope of a component when Quarkus is running.

Service

Using service layer in Quarkus is actually optional. You can indeed inject the repository class directly into the resource/controller, but in this project, I’m trying to use the MVC approach. Here is an example of a service class.

@ApplicationScoped
public class PersonService {

@Inject
PersonRepository personRepository;

@Transactional
public void create(PersonRequest request) {
Person person = new Person();

person.setFirstname(request.firstname());
person.setLastname(request.lastname());
person.setAddress(request.address());
personRepository.persist(person);
}

public Person getById(Long id) {
return personRepository.findByIdOptional(id)
.orElseThrow(() -> new IllegalArgumentException("Person with id " + id + " not found"));
}

public List<Person> getAll() {
return personRepository.listAll();
}

@Transactional
public void update(Long id, PersonRequest request) {
personRepository.findByIdOptional(id)
.ifPresent(person -> {
person.setFirstname(request.firstname());
person.setLastname(request.lastname());
person.setAddress(request.address());
personRepository.persist(person);
});
}

@Transactional
public void delete(Long id) {
personRepository.deleteById(id);
}
}

We don’t need to manually inject using the constructor; simply add the @Inject annotation to the repository variable. There are several methods used to perform operations on the database, such as persist(), findByIdOptional(), and deleteById(). The @Transactional annotation is used to handle write operations on the database, so that if an error occurs in any of the queries, all operations will be rolled back.

Resource

Resources play a crucial role as the bridge for request-response data; all data traffic will be handled and filtered here. Utilize the @Path annotation to register endpoints and inject the service class.

@Path("api/v1/persons")
public class PersonResource {

@Inject
PersonService personService;

}

The @Inject annotation for the service class is used so that CRUD operations present in that class can be utilized in the Resource class. This implies that the Resource class heavily depends on the service class. If you are not using the service class, simply inject the repository class directly. Then, add CRUD operations to the resource class:

@Path("api/v1/persons")
@Tag(name = "Person Resource", description = "REST API with Quarkus")
public class PersonResource {

@Inject
PersonService personService;

@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response create(PersonRequest person) {
personService.create(person);

return Response.ok().build();
}

@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("{id}")
public Response getById(@PathParam("id") Long id) {
return Response.ok(personService.getById(id)).build();
}

@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getAll() {
return Response.ok(personService.getAll()).build();
}

@PUT
@Produces(MediaType.APPLICATION_JSON)
@Path("{id}")
public Response update(@PathParam("id") Long id, PersonRequest person) {
personService.update(id, person);
return Response.ok().build();
}

@DELETE
@Produces(MediaType.APPLICATION_JSON)
@Path("{id}")
public Response delete(@PathParam("id") Long id) {
personService.delete(id);
return Response.ok().build();
}
}

Every CRUD operation has produces and consumes annotations. Produces means that the output generated in the API must be in JSON format, and consumes is added to write database operations such as adding, modifying, or deleting data, indicating that the input data must be in JSON format.

Quarkus Test & Mock Test

Adding testing to a project is a crucial parameter. The quality of an app is considered excellent if it includes testing and flexibility in testing program errors/bugs. Quarkus has its own test handling. In this article, we will use Quarkus test to test the endpoints of a resource. Before that, make sure that several testing libraries have been added to Maven/Gradle.

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-panache-mock</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>

Then create a class representation for testing resources in the test directory.

@QuarkusTest
@TestHTTPEndpoint(PersonResource.class)
public class PersonResourceTest {

@InjectSpy
PersonService personService;

}

@QuarkusTest annotation is used to mark a class as intended for testing. Then, other annotations like @TestHTTPEndpoint with the class resource argument are utilized to read the endpoints present in that class, eliminating the need to manually input available endpoints. The @InjectSpy annotation serves as metadata that monitors the behavior of the original object without altering it.

In other words, a spy allows us to intercept method calls and observe interactions with the original object without modifying it. Additionally, you can perform mocking on a service class using the @InjectMock annotation, which is used to replace the original object while controlling its behavior. This enables us to specify specific behavior for certain method calls and test interactions with the object.

Subsequently, each CRUD operation is tested using the rest-assured and Mockito libraries.

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;

@QuarkusTest
@TestHTTPEndpoint(PersonResource.class)
public class PersonResourceTest {

@InjectSpy
PersonService personService;

@Test
void testGetPersonsEndpoint() {
given().when().get()
.then()
.statusCode(200);

Mockito.verify(personService, Mockito.times(1)).getAll();
}

@Test
void testGetPersonByIdEndpoint() {
given()
.pathParam("id", 1L)
.when().get("{id}")
.then()
.body("firstname", equalTo("Ichwan"))
.statusCode(200);

Mockito.verify(personService, Mockito.times(1)).getById(1L);
}

@Test
void testCreatePersonEndpoint() {

PersonRequest personRequest = new PersonRequest("Ahmad", "Abdullah", "Metro");

given()
.contentType("application/json")
.body(personRequest)
.when().post()
.then().statusCode(200);

Mockito.verify(personService, Mockito.times(1))
.create(personRequest);
}

@Test
void testUpdatePersonEndpoint() {

PersonRequest personRequest = new PersonRequest("Budi", "Raharjo", "Metro");
var id = 1L;

given()
.contentType("application/json")
.body(personRequest)
.when().put("{id}",id)
.then()
.statusCode(200);

Mockito.verify(personService, Mockito.times(1))
.update(id, personRequest);
}

@Test
void testDeletePersonEndpoint() {
var id = 1L;

given()
.pathParam("id",id)
.when().delete("{id}")
.then().statusCode(200);

Mockito.verify(personService, Mockito.times(1)).delete(id);
}
}

Rest-assured ensures that every endpoint in the resource class runs as expected, while Mockito verifies the method calls in the service class by ensuring that method calls are only made once.

Reference:

Follow me:

Stackademic

Thank you for reading until the end. Before you go:

--

--