Recently, I have been doing much of framework and language hopping out of boredom. I have heard about Quarkus around a year ago. But I did not experiment with it much. Today, I had some free time and created a project with it to check how it measures against Spring Boot. In this article, I write about building REST APIs with Quarkus.
Introduction
Quarkus is a Cloud Native, (Linux) Container First framework for writing Java applications. Quarkus is an open-source project that started by Red Hat. It’s been around for a while but gained some popularity since last year. To put it in a simple term, Quarkus is a framework similar to Spring Boot that’s hugely optimized for the Cloud Native environment.
The key selling point of Quarkus is its fast start time and small memory footprint, especially when it’s compiled as a native app using GraalVM.
Famous User REST APIs example
To implement REST APIs, as usual, we will use the Users
example with the following APIs,
GET /v1/users
– returns a list of usersGET /v1/users/:id
– gets a user by idPOST /v1/users
– creates a new userPUT /v1/users/:id
– update an existing userDELETE /v1/users/:id
– deletes a user by id
For now, we don’t touch parts related to persistence and the service layer. We just focus on creating dummy REST APIs with some rudimentary error handling and payload validation.
Scaffolding the project
Similar to start.spring.io which allows you to bootstrap the project quickly, Quarkus also has that. Just open your browser and go to code.quarkus.io and select the following dependencies,
RESTEasy JSON-B
– for JSON serialization and deserializationHibernate Validator
– to validate payloadsHibernate ORM
– for persistence (for upcoming tutorials)JDBC Driver - MySQL
– to connect to JDBC (for upcoming tutorials)
After that, click the generate
button and download the zip file. Finally, open the project in your favorite IDE.
Building User REST APIs
As mentioned above we will create multiple APIs related to the user resource. So it makes sense to create the UserController
or UserResource
file and add some basic functionalities to it,
@Path("/v1/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserController {
private static final SortedSet<User> dummyUsers = new TreeSet<>();
static {
dummyUsers.addAll(Set.of(
createDummyUser(1, "Leonardo", "DiCaprio", 45),
createDummyUser(2, "Will", "Smith", 51),
createDummyUser(3, "Denzel", "Washington", 65))
);
}
private static User createDummyUser(int id, String firstName, String lastName, int age) {
User user = new User();
user.setId(id);
user.setFirstName(firstName);
user.setLastName(lastName);
user.setAge(age);
return user;
}
}
Some points about the above code,
@Path("/v1/users")
equals to@RequestMapping("/v1/users")
in Spring Boot.dummyUsers
represents the persistence that we will replace it with the actual database in upcoming posts.createDummyUser
method creates a user and adds them to thedummyUsers
set.
The User.java
entity/model looks like this,
package com.madadipouya.quarkus.example.entity;
public class User implements Comparable<User> {
private int id;
private String firstName;
private String lastName;
private int age;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public int compareTo(User o) {
return id - o.getId();
}
}
As of now, User.java
is just a POJO with no annotation except the compareTo
implementation that’s used for sorting the dummyUsers
set. Once again, we will add more functionalities to it in the following articles.
Now let’s go back to UserController
and start implementing the APIs.
GET /v1/users
implementation
The implementation is so simple. All we have to do is to return a list of users in the set.
@Path("/v1/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserController {
private static final SortedSet<User> dummyUsers = new TreeSet<>();
static {
dummyUsers.addAll(Set.of(
createDummyUser(1, "Leonardo", "DiCaprio", 45),
createDummyUser(2, "Will", "Smith", 51),
createDummyUser(3, "Denzel", "Washington", 65))
);
}
@GET
public Set<User> getUsers() {
return dummyUsers;
}
}
@GET
is equal to @GetMapping
in Spring Boot.
GET /v1/users/{id}
implementation
For this endpoint, we need to get a user id and loop through the set (dummyUsers
). If the user is found, returns it, otherwise, throws an exception which is translated to HTTP STATUS 404
,
@Path("/v1/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserController {
private static final SortedSet<User> dummyUsers = new TreeSet<>();
static {
dummyUsers.addAll(Set.of(
createDummyUser(1, "Leonardo", "DiCaprio", 45),
createDummyUser(2, "Will", "Smith", 51),
createDummyUser(3, "Denzel", "Washington", 65))
);
}
@GET
@Path("/{id}")
public User getUser(@PathParam("id") int id) throws UserNotFoundException {
return getUserById(id);
}
private User getUserById(int id) throws UserNotFoundException {
return dummyUsers.stream().filter(user -> user.getId() == id).findFirst()
.orElseThrow(() -> new UserNotFoundException("The user doesn't exist"));
}
}
package com.madadipouya.quarkus.example.exception;
public class UserNotFoundException extends Exception {
public UserNotFoundException(String message) {
super(message);
}
}
Since we need to loop through the set for PUT
and DELETE
methods, it makes sense to extract it in a method (getUserById
).
If you run the code as-is and provide a wrong id, the app returns a stack trace instead of 404
. That’s because we have not implemented ExceptionMapper
AKA Exception Handler (in Spring Boot term) yet.
For that, we need to create a class that implements ExceptionMapper
interface as follows,
package exceptionhandler;
import com.madadipouya.quarkus.example.exception.UserNotFoundException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class ExceptionHandler implements ExceptionMapper<Exception> {
@Override
public Response toResponse(Exception exception) {
if(exception instanceof UserNotFoundException) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponseBody(exception.getMessage()))
.build();
}
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponseBody("Something unexpected happened. Try again"))
.build();
}
public static final class ErrorResponseBody {
private final String message;
public ErrorResponseBody(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
}
We have made the use of the Response
builder to create the proper status code and message body. The ErrorResponseBody
class contains the error message.
POST /v1/users
implementation
We need to create a DTO that an API consumer can use to post payloads. This should differ from the User
entity/model because we don’t want the consumer to determine the id and expose the underlying implementation. For that, let’s create the UserDto
as an inner class, as of now,
public static class UserDto {
@NotBlank
private String firstName;
@NotBlank
private String lastName;
@Min(value = 1, message = "The value must be more than 0")
@Max(value = 200, message = "The value must be less than 200")
private int age;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
Note that the UserDto
must have public getters, setters, and default constructor, otherwise, Quarkus cannot populate the fields. We can also add some validations to it using the Hibernate Validator annotations.
Finally, we can implement the endpoint as follows,
@Path("/v1/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserController {
private static final SortedSet<User> dummyUsers = new TreeSet<>();
static {
dummyUsers.addAll(Set.of(
createDummyUser(1, "Leonardo", "DiCaprio", 45),
createDummyUser(2, "Will", "Smith", 51),
createDummyUser(3, "Denzel", "Washington", 65))
);
}
@POST
public User createUser(@Valid UserDto userDto) {
User user = createDummyUser(dummyUsers.last().getId() + 1, userDto.firstName, userDto.lastName, userDto.age);
dummyUsers.add(user);
return user;
}
}
@Valid
handles the validation, if any constraints are violated, it returns 400
with proper error messages.
PUT /v1/users/{id}
implementation
This endpoint is very similar to POST
and rather straightforward,
@Path("/v1/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserController {
private static final SortedSet<User> dummyUsers = new TreeSet<>();
static {
dummyUsers.addAll(Set.of(
createDummyUser(1, "Leonardo", "DiCaprio", 45),
createDummyUser(2, "Will", "Smith", 51),
createDummyUser(3, "Denzel", "Washington", 65))
);
}
@PUT
@Path("/{id}")
public User updateUser(@PathParam("id") int id, @Valid UserDto userDto) throws UserNotFoundException {
User user = getUserById(id);
user.setFirstName(userDto.firstName);
user.setLastName(userDto.lastName);
user.setAge(userDto.age);
return user;
}
private User getUserById(int id) throws UserNotFoundException {
return dummyUsers.stream().filter(user -> user.getId() == id).findFirst()
.orElseThrow(() -> new UserNotFoundException("The user doesn't exist"));
}
}
DELETE /v1/users/{id}
implementation
We can code DELETE
endpoint as follows,
@Path("/v1/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserController {
private static final SortedSet<User> dummyUsers = new TreeSet<>();
static {
dummyUsers.addAll(Set.of(
createDummyUser(1, "Leonardo", "DiCaprio", 45),
createDummyUser(2, "Will", "Smith", 51),
createDummyUser(3, "Denzel", "Washington", 65))
);
}
@DELETE
@Path("/{id}")
public Response deleteUser(@PathParam("id") int id) throws UserNotFoundException {
dummyUsers.remove(getUserById(id));
return Response.status(Response.Status.NO_CONTENT).build();
}
private User getUserById(int id) throws UserNotFoundException {
return dummyUsers.stream().filter(user -> user.getId() == id).findFirst()
.orElseThrow(() -> new UserNotFoundException("The user doesn't exist"));
}
}
Note that, we return an instance of Response
. This is because we want to return 204
status code instead of 200
.
Test the application
Now that the implementation completed, you can run the application,
$ ./mvnw quarkus:dev
Protip: you don’t need to stop the application after every change. Simply save the file and refresh the page 😀 Quarkus takes care of it amazingly.
And then test it with the following CURL commands,
# get list of users
$ curl localhost:8080/v1/users/
# get a specific user
$ curl localhost:8080/v1/users/2
# create a user
$ curl --request POST 'localhost:8080/v1/users' --header 'Content-Type: application/json' \
--data-raw '{
"firstName": "Tom",
"lastName": "Cruise",
"age": 57
}'
# edit a user
$ curl --request PUT 'localhost:8080/v1/users/1' --header 'Content-Type: application/json' \
--data-raw '{
"firstName": "Leonardo",
"lastName": "DiCaprio",
"age": 46
}'
# delete a user
$ curl --request DELETE 'localhost:8080/v1/users/2'
You can also find the complete working example on my GitHub,
https://github.com/kasramp/quarkus-rest-example
We are done with building REST APIs with Quarkus, it’s very easy as you’ve seen. In the next article, I go through building the service layer and persisting the data to MySQL.