Building REST APIs with Quarkus

Building REST APIs with Quarkus

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 users
  • GET /v1/users/:id – gets a user by id
  • POST /v1/users – creates a new user
  • PUT /v1/users/:id – update an existing user
  • DELETE /v1/users/:id – deletes a user by id
Build REST APIs with Quarkus

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 deserialization
  • Hibernate Validator – to validate payloads
  • Hibernate 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 the dummyUsers 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.

Inline/featured images credits

  • Spring Boot logo by Spring
  • Quarkus logo by Quarkus
  • Duel background (a painting circa 1820 depicts a duel) unnamed artist, possibly Robert Cruikshank