Bootstrap a Quarkus project with REST API

Creating a Quarkus Application

There are several ways to create a Quarkus project and all of them are valid:

For the sake of simplicity, we are going to use the later one. Open a browser and navigate to https://code.quarkus.io/.

There you can set the name of the artifact (todo-app) and select the RESTEasy JAX-RS endpoint to create a REST API service. In the following capture, you see page with all the modifications described above:

todo app gen

The push Generate your application button, download the zip file into a folder, and unzip it.

unzip todo-app.zip
cd todo-app

Open the project in your favourite IDE, and open the file src/main/java/org/acme/GreetingResource.java to see how to define a REST endpoint in Quarkus:

package org.acme;

import javax.ws.rs.GET; (1)
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/hello") (2)
public class GreetingResource {

    @GET (3)
    @Produces(MediaType.TEXT_PLAIN) (4)
    public String hello() {
        return "Hello RESTEasy"; (5)
    }
}
1 Imports from JAX-RS spec
2 @Path to set the endpoint for this resource
3 HTTP method to use.
4 Mime-Type of produced message
5 A simple text message

Let’s query this REST API endpoint.

Quarkus dev mode

Live Coding or Development mode enables hot deployment with background compilation, which means that when you modify your Java files and/or your resource files and refresh your browser, these changes will automatically take effect.

This is probably one of the best features of Quarkus: enabling a very fast and productive iterative feedback loop.

Let’s start the Live Coding mode by invoking mvnw from the project directory.

Open a terminal window and run the following command:

./mvnw compile quarkus:dev

After a second or so, you’ll have the application started in dev mode.

[INFO] Scanning for projects...
....
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2022-03-22 10:46:44,424 INFO  [io.quarkus] (Quarkus Main Thread) todo-app 1.0.0-SNAPSHOT on JVM (powered by Quarkus 2.7.5.Final) started in 1.743s. Listening on: http://localhost:8080

2022-03-22 10:46:44,440 INFO  [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2022-03-22 10:46:44,441 INFO  [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, resteasy, smallrye-context-propagation, vertx]

In a new terminal window send a request to http://localhost:8080/hello and you’ll get the result:

http localhost:8080/hello
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
content-length: 14

Hello RESTEasy

Get back to your IDE without stopping the service and change the return message from Hello RESTEasy to Hello World.

@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
    return "Hello World";
}

And in the terminal window send again a request to http://localhost:8080/hello

http localhost:8080/hello
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
content-length: 11

Hello World

Notice that without having to stop the service, nor recompiling, the service is updated and the requests returns the modification we’ve done in the code.

Quarkus Continuous Testing

So far, we’ve developed a simple REST endpoint, but how about testing it? Open the following file src/test/java/org/acme/GreetingResourceTest.java to see an example of a Quarkus test:

package org.acme;

import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given; (1)
import static org.hamcrest.CoreMatchers.is;

@QuarkusTest (2)
public class GreetingResourceTest {

    @Test
    public void testHelloEndpoint() {
        given() (3)
          .when().get("/hello") (4)
          .then()
             .statusCode(200)
             .body(is("Hello RESTEasy")); (5)
    }

}
1 Although not mandatory, Quarkus uses RestAssured to test Rest APIs
2 This annotations boots up the application before executing the tests
3 The location of the service (host:port) is automatically configured
4 Creates a GET HTTP request to /hello
5 Asserts that the message is Hello RESTEasy

Go back to terminal window (navigate to todo-app directory if not already there) and run the test executing the test command:

./mvnw test

And the output is similar to:

[INFO] Running org.acme.GreetingResourceTest
2022-03-22 11:53:25,466 INFO  [org.jbo.threads] (main) JBoss Threads version 3.4.2.Final
2022-03-22 11:53:26,420 INFO  [io.quarkus] (main) Quarkus 2.7.5.Final on JVM started in 1.363s. Listening on: http://localhost:8081 (1)
2022-03-22 11:53:26,420 INFO  [io.quarkus] (main) Profile test activated.
2022-03-22 11:53:26,421 INFO  [io.quarkus] (main) Installed features: [cdi, resteasy, smallrye-context-propagation, vertx]
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 4.219 s <<< FAILURE! - in org.acme.GreetingResourceTest
[ERROR] org.acme.GreetingResourceTest.testHelloEndpoint  Time elapsed: 1.118 s  <<< FAILURE! (2)
java.lang.AssertionError:
1 expectation failed.
Response body doesn t match expectation.
Expected: is "Hello RESTEasy"
  Actual: Hello World
	at org.acme.GreetingResourceTest.testHelloEndpoint(GreetingResourceTest.java:18)
1 Tests are started to port 8081 by default, so no worries on the previous running instance in dev mode
2 Obviously test is failing as the we changed the message

Let’s fix the test by changing the expected message to Hello World.

@Test
public void testHelloEndpoint() {
    given()
    .when().get("/hello")
    .then()
    .statusCode(200)
    .body(is("Hello World"));
}

Run again the test goal to validate that we’ve fixed the test.

./mvnw test

And the output is similar to:

[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.231 s - in org.acme.GreetingResourceTest
2022-03-22 11:59:37,279 INFO  [io.quarkus] (main) Quarkus stopped in 0.041s
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

This is the normal way we’ve used for years in Java for executing tests, but Quarkus comes with one thing called Continuous Testing, meaning that your tests will run immediately after code changes were saved.

Go back to the terminal window where you started the application in dev mode at the beginning of this section, and down the bottom of the screen you should see the following:

Tests paused
Press [r] to resume testing, [o] Toggle test output, [:] for the terminal, [h] for more options>

Press r and the tests will start running. You should see the status change down the bottom of the screen as they are running, and it should finish similar to this:

All 1 test is passing (0 skipped), 1 test was run in 239ms. Tests completed at 12:03:37 due to changes to GreetingResource.class.

So all tests have run and they are passing. But now, we’re in continuous testing mode which means that any change we do in the code, Quarkus will detect tests affected by the change, and re-run them automatically.

Go to your IDE without stopping the service, open GreetingResource.java, and change the return message from Hello World to Hi World.

Then save the file and go back to the terminal window where the application wa started in dev mode. Automatically, you’ll see the following message in the console indicating that there is one test failure:

2022-03-22 11:24:47,216 INFO  [io.qua.dep.dev.RuntimeUpdatesProcessor] (vert.x-worker-thread-0) Live reload total time: 0.742s
2022-03-22 12:14:06,118 ERROR [io.qua.test] (Test runner thread) ==================== TEST REPORT #3 ====================
2022-03-22 12:14:06,118 ERROR [io.qua.test] (Test runner thread) Test GreetingResourceTest#testHelloEndpoint() failed
: java.lang.AssertionError: 1 expectation failed.
Response body doesnt match expectation.
Expected: is "Hello World"
  Actual: Hi World

Now open GreetingResourceTest and update the expecting message to be Hi World, and inspect again the console

2022-03-22 12:16:50,783 INFO  [io.qua.test] (Test runner thread) All tests are now passing
--
All 1 test is passing (0 skipped), 1 test was run in 316ms. Tests completed at 12:16:50 due to changes to GreetingResourceTest.class.

Tests are back to normal.

Configuration

In Quarkus, there is a central place where you can configure the application. Let’s make the greeting message configurable. Open the src/main/resources/application.properties file and add the following property:

greeting.message=Hi World

Then modify GreetingResource.java class to inject this property value:

@ConfigProperty(name = "greeting.message") (1) (2)
String msg;

@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
    return msg; (3)
}
1 Import org.eclipse.microprofile.config.inject.ConfigProperty class
2 Injects the value set in the greeting.message property
3 Returns the message value

Before jumping to the following section, stop the application running instance by pressing q char.

This is a really simple example, let’s move things forward and create a TODO application.

RestEasy Jackson/OpenAPI

The first thing before starting developing the TODO app is registering two new extensions, one for adding the Jackson support so marshalling/unmarshalling operations from/to Java objects from/to JSON documents, happens automatically.

Moreover, we’re going to add the OpenAPI extension so a Swagger UI interface is deployed automatically to test the API.

Run the following command in the project directory:

./mvnw quarkus:add-extension -Dextensions="io.quarkus:quarkus-resteasy-jackson,quarkus-smallrye-openapi"

And both extensions are added:

[INFO] [SUCCESS] ✅  Extension io.quarkus:quarkus-resteasy-jackson has been installed
[INFO] [SUCCESS] ✅  Extension io.quarkus:quarkus-smallrye-openapi has been installed

Create a new class named Todo.java representing the todo data.

package org.acme;

public class Todo {
    public Long id;
    public String title;
    public boolean completed;
    public int order;
    public String url;
}

Then create a new REST API resource named TodoResource.java to implement all required operations for a TODO application:

package org.acme;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.OPTIONS;
import javax.ws.rs.PATCH;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

@Path("/api")
public class TodoResource {

    private Map<Long, Todo> todos = new HashMap<>(); (1)

    (2)

    @OPTIONS
    public Response opt() {
        return Response.ok().build();
    }

    @GET (3) (4)
    public List<Todo> getAll() {
        return new ArrayList<>(todos.values());
    }

    @GET
    @Path("/{id}")
    public Todo getOne(@PathParam("id") Long id) {
        Todo entity = todos.get(id);
        if (entity == null) {
            throw new WebApplicationException("Todo with id of " + id + " does not exist.", Status.NOT_FOUND);
        }
        return entity;
    }

    @POST
    public Response create(Todo item) { (5)
        todos.put(item.id, item);
        return Response.status(Status.CREATED).entity(item).build();
    }

    @PATCH
    @Path("/{id}")
    public Response update(Todo todo, @PathParam("id") Long id) {
        Todo entity = todos.get(id);
        entity.id = id;
        entity.completed = todo.completed;
        entity.order = todo.order;
        entity.title = todo.title;
        entity.url = todo.url;
        return Response.ok(entity).build();
    }

    @DELETE
    public Response deleteCompleted() {
        todos.entrySet().removeIf(e -> e.getValue().completed);
        return Response.noContent().build();
    }

    @DELETE
    @Path("/{id}")
    public Response deleteOne(@PathParam("id") Long id) {
        Todo entity = todos.get(id);
        if (entity == null) {
            throw new WebApplicationException("Todo with id of " + id + " does not exist.", Status.NOT_FOUND);
        }
        todos.remove(id);
        return Response.noContent().build();
    }
}
1 Stores list of Todos in memory
2 List of operations
3 Marshalling from Java object to JSON is automatically done
4 Content Type is automatically set to JSON
5 Unmarshalling from JSON to object is automatically done

Start the application again in dev mode running the following command:

./mvnw compile quarkus:dev

When the application is running, open a browser and navigate to http://localhost:8080/q/swagger-ui/.

todo swagger

Then open the POST /api section and push the Try It Out button. And use the Swagger UI to insert a new TODO:

todo swagger insert

Or you can use curl for the same thing:

curl -X 'POST' \
  'http://localhost:8080/api' \
  -H 'accept: */*' \
  -H 'Content-Type: application/json' \
  -d '{
  "id": 1,
  "title": "Quarkus",
  "completed": false,
  "order": 0,
  "url": "quarkus.io"
}'

Also, the same can be used to get the list of TODOs.

todo swagger get

Or use curl:

curl localhost:8080/api

At this point, we’ve seen how to test the application backend using Swagger UI.

But there is no frontend yet for the application. Quarkus lets you copy HTML resources to src/main/resources/META-INF/resources directory and serving them as static resources.

We’ve developed a UI for the TODO application, download the zip file containing the UI from GitHub.

In a terminal window, go to src/main/resources/META-INF directory, copy the resources.zip file there, and unzip it.

cd src/main/resources/META-INF

unzip resources.zip

Without restarting the application, open a browser and navigate to http://localhost:8080/todo.html

todo app f

And you can start interacting with the UI adding TODOs, completing them, …​

Oh you are coming from Spring Boot, no worries

In this example, we used JAX-RS annotations and MicroProfile Config annotations for defining the endpoint, but if you are coming from Spring stack, you might find a barrier to learn new annotations.

Quarkus offers an integration to Spring annotations, so they are used in the code, and at compilation time they are transformed to "Quarkus" annotations.

Go back to the terminal where you started the appication (./mvnw compile quarkus:dev) and stop it. Then run the following command to add the integration with Spring stack:

./mvnw quarkus:add-extension -Dextensions="io.quarkus:quarkus-spring-web,io.quarkus:quarkus-spring-di"

After this addition, we can start using Spring DI and Spring Web MVC annotations.

Substitue the GreetingResource.java class with the following content. It’s exactly the same resource but using Spring annotations:

package org.acme;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GreetingResource {

    @Value("greeting.message")
    String msg;

    @GetMapping(path = "/hello")
    public String hello() {
        return msg;
    }
}

See that only Spring Framework imports are present. Start again the application in dev mode and make a request:

./mvnw compile quarkus:dev
curl localhost:8080/hello
Hi World

Clean Up

Go back to the terminal where you started the appication (./mvnw compile quarkus:dev) and stop it.