Exercise - Create a Quarkus application

Completed

In this unit, you create a basic Quarkus application. You use Maven to bootstrap the application and an integrated development environment (IDE) of your choice to edit the code. Use the Bash environment configured in the prerequisites to run the code. You use Docker to start a local PostgreSQL database so you can run and test the application locally.

Generate the Quarkus application by using Maven

There are several ways to generate a Quarkus project structure. You can use the Quarkus web interface, an IDE plugin, or the Quarkus Maven plugin. Let's use the Maven plugin to generate the project structure.

You generate your application with several dependencies:

  • The resteasy dependency to expose a REST endpoint
  • The jackson dependency to serialize and deserialize JSON
  • The hibernate dependency to interact with the database
  • The postgresql dependency to connect to the PostgreSQL database
  • The quarkus-container-image-docker dependency to build a Docker image

You don't need to specify Azure dependencies because you run your application locally first and then deploy a containerized version of it to Azure Container Apps.

Note

The commands in this module use Bash syntax. Don't paste them unchanged into PowerShell or Windows Command Prompt. On Windows, run the Bash commands in the configured WSL distribution. Use another local Bash terminal for HTTP requests while the application is running.

Before generating the project, check that your Bash environment uses Java 17 and Maven 3.9.9:

java -version
mvn --version

The initial generator command uses your installed Maven. The project's Maven wrapper is created afterward, so it doesn't replace this prerequisite. Select the required Java and Maven versions before continuing.

From the directory where you want to create the project, generate the to-do application:

mvn -U io.quarkus:quarkus-maven-plugin:3.19.0:create \
    -DplatformVersion=3.18.4 \
    -DprojectGroupId=com.example.demo \
    -DprojectArtifactId=todo \
    -DclassName="com.example.demo.TodoResource" \
    -Dpath="/api/todos" \
    -DjavaVersion=17 \
    -Dextensions="resteasy-jackson, hibernate-orm-panache, jdbc-postgresql, io.quarkus:quarkus-container-image-docker"

This command creates a new Quarkus project in a directory named todo. It generates a Maven directory structure (src/main/java for source code and src/test/java for tests). It creates some Java classes, some tests, and some Dockerfiles. It also generates a pom.xml file with all the needed dependencies (Hibernate, RESTEasy, Jackson, PostgreSQL, and Docker):

  <dependencies>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-hibernate-orm-panache</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-resteasy-jackson</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-jdbc-postgresql</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-container-image-docker</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-arc</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-hibernate-orm</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-resteasy</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-junit5</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>io.rest-assured</groupId>
      <artifactId>rest-assured</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

Note

All the dependencies in the pom.xml file are defined in the Quarkus BOM (bill of materials) io.quarkus.platform:quarkus-bom.

Code the application

Next, rename the generated MyEntity.java class to Todo.java (located in the same folder as the TodoResource.java file). Replace the existing code with the following Java code. It uses Java Persistence API (jakarta.persistence.* package) to store and retrieve data from your PostgreSQL server. It also uses Hibernate ORM with Panache (inheriting from io.quarkus.hibernate.orm.panache.PanacheEntity) to simplify the persistence layer.

You use a JPA entity (@Entity) to map the Java Todo object directly to the PostgreSQL Todo table. The TodoResource REST endpoint then creates a new Todo entity class and persists it. This class is a domain model that's mapped on the Todo table. The table is automatically created by JPA.

Extending PanacheEntity gets you a number of generic create, read, update, and delete (CRUD) methods for your type. So you can do things like saving and deleting Todo objects in just one line of Java code.

Add the following code to the Todo entity:

package com.example.demo;

import io.quarkus.hibernate.orm.panache.PanacheEntity;

import jakarta.persistence.Entity;
import java.time.Instant;

@Entity
public class Todo extends PanacheEntity {

    public String description;

    public String details;

    public boolean done;

    public Instant createdAt = Instant.now();

    @Override
    public String toString() {
        return "Todo{" +
                "id=" + id + '\'' +
                ", description='" + description + '\'' +
                ", details='" + details + '\'' +
                ", done=" + done +
                ", createdAt=" + createdAt +
                '}';
    }
}

To manage that class, update the TodoResource so that it can publish REST interfaces to store and retrieve data by using HTTP. Open the TodoResource class and replace the code with the following:

package com.example.demo;

import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;
import jakarta.ws.rs.core.UriInfo;
import org.jboss.logging.Logger;

import java.util.List;

@Path("/api/todos")
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
public class TodoResource {

    @Inject
    Logger logger;

    @Inject
    UriInfo uriInfo;

    @POST
    @Transactional
    public Response createTodo(Todo todo) {
        logger.info("Creating todo: " + todo);
        Todo.persist(todo);
        UriBuilder uriBuilder = uriInfo.getAbsolutePathBuilder().path(todo.id.toString());
        return Response.created(uriBuilder.build()).entity(todo).build();
    }

    @GET
    public List<Todo> getTodos() {
        logger.info("Getting all todos");
        return Todo.listAll();
    }

    @GET
    @Path("/{id}")
    public Todo getTodo(@PathParam("id") Long id) {
        Todo todo = id == null ? null : Todo.findById(id);
        if (todo == null) {
            throw new NotFoundException();
        }
        return todo;
    }
}

The POST method returns the new item's URI in the Location header. The item GET method serves that URI and returns HTTP 404 if the item isn't found. The null guard also handles path values that RESTEasy converts to a null identifier, without passing null to Panache.

Run the application

When you run the application in development mode, Docker needs to be running. That's because Quarkus detects that you need a PostgreSQL database (because of the PostgreSQL dependency quarkus-jdbc-postgresql declared in the pom.xml file), downloads the PostgreSQL Docker image, and starts a container with the database. It then automatically creates the Todo table in the database.

Note

The pinned Quarkus 3.18.4 platform uses Testcontainers 1.20.4, whose Java Docker client defaults to API 1.32. Docker Engine 29.x no longer accepts that API version by default. If you're using Engine 29.x, run docker version and confirm that the server's supported API range includes 1.44. Only if it does, run the following command in this Bash shell before starting Quarkus or tests:

export JDK_JAVA_OPTIONS="${JDK_JAVA_OPTIONS:+$JDK_JAVA_OPTIONS }-Dapi.version=1.44"

Keep this setting in the same shell for the test and later package commands. JDK_JAVA_OPTIONS supplies the Java system property to Java launcher processes without changing the pinned Quarkus versions. Setting DOCKER_API_VERSION alone doesn't configure this version of the Java Docker client. Don't force API 1.44 on a server whose supported range excludes it; use a separately validated compatible Docker and client configuration instead.

Make sure Docker is running and accessible from your configured Bash environment. In the terminal where you generated the project, enter the todo directory and start the application:

cd todo
./mvnw quarkus:dev

Run all subsequent Maven commands from this generated todo directory.

The Quarkus application should start and connect to your database. You should see the following output:

2025-02-28 08:38:33,418 INFO  [io.qua.dat.dep.dev.DevServicesDatasourceProcessor] (build-28) Dev Services for default datasource (postgresql) started - container ID is ce37977203b0
2025-02-28 08:38:33,421 INFO  [io.qua.hib.orm.dep.dev.HibernateOrmDevServicesProcessor] (build-6) Setting quarkus.hibernate-orm.database.generation=drop-and-create to initialize Dev Services managed database
__  ____  __  _____   ___  __ ____  ______ 
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ 
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \   
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/   
2025-02-28 08:38:35,278 INFO  [io.quarkus] (Quarkus Main Thread) todo 1.0.0-SNAPSHOT on JVM (powered by Quarkus 3.18.4) started in 5.367s. Listening on: http://localhost:8080

2025-02-28 08:38:35,280 INFO  [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2025-02-28 08:38:35,280 INFO  [io.quarkus] (Quarkus Main Thread) Installed features: [agroal, cdi, hibernate-orm, hibernate-orm-panache, jdbc-postgresql, narayana-jta, resteasy, resteasy-jackson, smallrye-context-propagation, vertx]

--
Tests paused
Press [e] to edit command line args (currently ''), [r] to resume testing, [o] Toggle test output, [:] for the terminal, [h] for more options>

To test the application, you can use cURL.

In a second local Bash terminal, create a new to-do item in the database with the following command. You should see the log in the Quarkus console:

curl --header "Content-Type: application/json" \
    --request POST \
    --data '{"description":"Take Quarkus MS Learn","details":"Take the MS Learn on deploying Quarkus to Azure Container Apps","done": "true"}' \
    http://127.0.0.1:8080/api/todos

This command should return the created item (with an identifier):

{"id":1,"description":"Take Quarkus MS Learn","details":"Take the MS Learn on deploying Quarkus to Azure Container Apps","done":true,"createdAt":"2025-02-26T07:27:30.093447Z"}

Create a second to-do by using the following cURL command:

curl --header "Content-Type: application/json" \
    --request POST \
    --data '{"description":"Take Azure Container Apps MS Learn","details":"Take the ACA Learn module","done": "false"}' \
    http://127.0.0.1:8080/api/todos

Next, retrieve the data by using a new cURL request:

curl http://127.0.0.1:8080/api/todos

This command returns the list of to-do items, including the items you created. Each item also includes createdAt; the following example omits those timestamps for brevity:

[ 
  {"id":1,"description":"Take Quarkus MS Learn","details":"Take the MS Learn on deploying Quarkus to Azure Container Apps","done":true},
  {"id":2,"description":"Take Azure Container Apps MS Learn","details":"Take the ACA Learn module","done":false}
]

Test the application

After the HTTP checks, return to the terminal running Quarkus and press Ctrl+C to stop development mode. Keep this Bash shell open and stay in the todo directory for the test command.

To test the application, you can use the existing TodoResourceTest class. It needs to test the REST endpoint. To test the endpoint, it uses RESTAssured. The tests retain the collection check, follow the Location returned by POST, and check missing and invalid item identifiers. Replace code in the TodoResourceTest class with the following code:

package com.example.demo;

import io.quarkus.test.junit.QuarkusTest;
import io.restassured.response.Response;
import static io.restassured.RestAssured.given;
import static jakarta.ws.rs.core.HttpHeaders.CONTENT_TYPE;
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.Test;

@QuarkusTest
class TodoResourceTest {

    @Test
    void shouldGetAllTodos() {
        given()
                .when().get("/api/todos")
                .then()
                .statusCode(200);
    }

    @Test
    void shouldCreateATodo() {
        Todo todo = new Todo();
        todo.description = "Take Quarkus MS Learn";
        todo.details = "Take the MS Learn on deploying Quarkus to Azure Container Apps";
        todo.done = true;

        Response created = given().body(todo)
                .header(CONTENT_TYPE, APPLICATION_JSON)
                .when().post("/api/todos")
                .then()
                .statusCode(201)
                .extract().response();

        String location = created.getHeader("Location");
        assertNotNull(location);
        long createdId = created.jsonPath().getLong("id");

        Response retrieved = given()
                .when().get(location)
                .then()
                .statusCode(200)
                .extract().response();

        assertEquals(createdId, retrieved.jsonPath().getLong("id"));
        assertEquals(todo.description, retrieved.jsonPath().getString("description"));
    }

    @Test
    void shouldReturnNotFoundForMissingTodo() {
        given()
                .when().get("/api/todos/-1")
                .then().statusCode(404);
    }

    @Test
    void shouldRejectInvalidTodoIds() {
        given()
                .when().get("/api/todos/not-a-number")
                .then().statusCode(404);

        given().urlEncodingEnabled(false)
                .when().get("/api/todos/%20")
                .then().statusCode(404);
    }
}

The create test reads the generated identifier from the POST response and compares both identifiers as long values. The invalid-identifier test disables automatic URL encoding for %20 so that the request contains the deliberately encoded space.

When you test the application, Docker needs to be running and accessible from the same Bash environment because Quarkus starts a separate PostgreSQL Dev Service for testing. In the terminal where you stopped Quarkus, test the application by using this command:

./mvnw clean test

You should see output that looks similar to this:

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.example.demo.TodoResourceTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

Keep this Bash shell open for the database exercise. You'll set AZ_POSTGRES_JDBC_URL, AZ_POSTGRES_USERNAME, and AZ_POSTGRES_PASSWORD there, then restart Quarkus from that shell. Stop the remote-development process with Ctrl+C before the later ./mvnw clean package and deployment steps. Keep the variables and don't repeat the resource-name and password setup between units.

A process inherits its environment when it starts. Exporting variables in another terminal doesn't update an already-running Quarkus process. Editing application.properties and relying on live reload isn't sufficient for the switch to Azure; restart the application from the shell where you set the variables.