How To Introduce a New API Quickly Using Quarkus and ChatGPT

John VesterJohn Vester
10 min read

My last two articles (part 1 and part 2) focused on getting to market quickly using Java. The only difference was the build automation tool that I used for each example. This time, I want to step outside of my comfort zone and try something a little different.

I read about how Quarkus is a Kubernetes-native Java framework designed for building fast, lightweight microservices. What’s even better is that it is optimized for cloud environments, including features like fast startup times, low memory footprints, and support for both imperative and reactive programming models.

Like before, the key to success is how quickly we can go from idea to reality. In this case, let’s see how quickly I can establish a new API using Quarkus and deploy it to the cloud.

Leaning on ChatGPT for Assistance … Again

I started this series by asking ChatGPT to help create an OpenAPI specification for my Motivational Quotes service. This time, I’m asking ChatGPT how to get started with Quarkus for another instance of this service.

ChatGPT guides me through a series of steps, starting with using the Quarkus CLI, which I install locally with Homebrew.

$ brew install quarkusio/tap/quarkus
$ quarkus --version
3.19.4

I use the CLI to create the new project:

$ quarkus create app com.example.quotes:quotes-quarkus

The CLI responds with the following output:

Looking for the newly published extensions in registry.quarkus.io
-----------

applying codestarts...
✓ java
✓ maven
✓ quarkus
✓ config-properties
✓ tooling-dockerfiles
✓ tooling-maven-wrapper
✓ rest-codestart

-----------
[SUCCESS] ✓  quarkus project has been successfully generated in:
--> /Users/johnvester/projects/jvc/quotes-quarkus
-----------
Navigate into this directory and get started: quarkus dev

We can see from scanning the root directory that Quarkus uses the Maven build automation tool by default:

$ cd quotes-quarkus && ls -la
total 96
drwxr-xr-x  11 johnvester    352 Mar 22 11:55 .
drwxrwxrwx  91 root         2912 Mar 22 11:55 ..
-rw-r--r--@  1 johnvester    6148 Mar 22 11:55 .DS_Store
-rw-r--r--   1 johnvester     75 Mar 22 11:55 .dockerignore
-rw-r--r--   1 johnvester    423 Mar 22 11:55 .gitignore
drwxr-xr-x   3 johnvester     96 Mar 22 11:55 .mvn
-rw-r--r--   1 johnvester   1793 Mar 22 11:55 README.md
-rwxr-xr-x   1 johnvester  11172 Mar 22 11:55 mvnw
-rw-r--r--   1 johnvester   7697 Mar 22 11:55 mvnw.cmd
-rw-r--r--   1 johnvester   5003 Mar 22 11:55 pom.xml
drwxr-xr-x   5 johnvester    160 Mar 22 11:55 src

Like the OpenAPI specification in my first article, ChatGPT provides the necessary steps to get me to a point where I can be successful.

Developing My Quarkus Service

To avoid duplication of effort, I’ll continue to use the OpenAPI specification from my first article.

Leveraging guidance from Quarkus documentation and my prior API-First experience, I update pom.xml as noted below:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example.quotes</groupId>
    <artifactId>quotes-quarkus</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <properties>
        <compiler-plugin.version>3.14.0</compiler-plugin.version>
        <lombok.version>1.18.36</lombok.version>
        <maven.compiler.release>21</maven.compiler.release>
        <openapi-generator-maven-plugin.version>7.12.0</openapi-generator-maven-plugin.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
        <quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
        <quarkus.platform.version>3.19.4</quarkus.platform.version>
        <skipITs>true</skipITs>
        <surefire-plugin.version>3.5.2</surefire-plugin.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>${quarkus.platform.group-id}</groupId>
                <artifactId>${quarkus.platform.artifact-id}</artifactId>
                <version>${quarkus.platform.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-arc</artifactId>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-resteasy-jackson</artifactId>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-smallrye-openapi</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <scope>provided</scope>
        </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>

    <build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*</include>
                </includes>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>${quarkus.platform.group-id}</groupId>
                <artifactId>quarkus-maven-plugin</artifactId>
                <version>${quarkus.platform.version}</version>
                <extensions>true</extensions>
                <executions>
                    <execution>
                        <goals>
                            <goal>build</goal>
                            <goal>generate-code</goal>
                            <goal>generate-code-tests</goal>
                            <goal>native-image-agent</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${compiler-plugin.version}</version>
                <configuration>
                    <parameters>true</parameters>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>1.18.36</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>${surefire-plugin.version}</version>
                <configuration>
                    <systemPropertyVariables>
                        <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
                        <maven.home>${maven.home}</maven.home>
                    </systemPropertyVariables>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-failsafe-plugin</artifactId>
                <version>${surefire-plugin.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>integration-test</goal>
                            <goal>verify</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <systemPropertyVariables>
                        <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
                        <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
                        <maven.home>${maven.home}</maven.home>
                    </systemPropertyVariables>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.openapitools</groupId>
                <artifactId>openapi-generator-maven-plugin</artifactId>
                <version>${openapi-generator-maven-plugin.version}</version>
                <executions>
                    <execution>
                        <id>quotes-api</id>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                        <configuration>
                            <inputSpec>${project.basedir}/src/main/resources/META-INF/openapi.yaml</inputSpec>
                            <configOptions>
                                <apiPackage>com.example.api</apiPackage>
                                <modelPackage>com.example.model</modelPackage>
                            </configOptions>
                        </configuration>
                    </execution>
                </executions>
                <configuration>
                    <output>${codegen.openapi.generated-sources-dir}</output>
                    <generatorName>jaxrs-spec</generatorName>
                    <generateApiTests>false</generateApiTests>
                    <generateModelTests>false</generateModelTests>
                    <generateModelDocumentation>false</generateModelDocumentation>
                    <generateApiDocumentation>false</generateApiDocumentation>
                    <configOptions>
                        <sourceFolder>src/main/java</sourceFolder>
                        <useJakartaEe>true</useJakartaEe>
                        <useSwaggerAnnotations>false</useSwaggerAnnotations>
                        <interfaceOnly>true</interfaceOnly>
                        <dateLibrary>java8</dateLibrary>
                        <openApiNullable>false</openApiNullable>
                        <invokerPackage>com.example.api</invokerPackage>
                    </configOptions>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <profiles>
        <profile>
            <id>native</id>
            <activation>
                <property>
                    <name>native</name>
                </property>
            </activation>
            <properties>
                <skipITs>false</skipITs>
                <quarkus.native.enabled>true</quarkus.native.enabled>
            </properties>
        </profile>
    </profiles>
</project>

This time, we place the openapi.yaml file into the resources/META-INF folder and create an application.properties file in the resources folder. The contents of the file are as follows:

quarkus.banner.path=banner.txt
quarkus.http.port=8080
mp.openapi.scan.disable=true

Our banner.txt file has the following contents:

                   _
  __ _ _   _  ___ | |_ ___  ___
 / _` | | | |/ _ \| __/ _ \/ __|
| (_| | |_| | (_) | ||  __/\__ \
 \__, |\__,_|\___/ \__\___||___/
    |_|

Create the Business Logic

Like in my prior articles, we’ll use in-memory data for our motivational quotes. In a newly-created repositories package, I add the QuotesRepository class, which is very similar to what we’ve used so far:

@ApplicationScoped
public class QuotesRepository {
    public static final List<Quote> QUOTES = List.of(
            new Quote()
                    .id(1)
                    .quote("The greatest glory in living lies not in never falling, but in rising every time we fall."),
            new Quote()
                    .id(2)
                    .quote("The way to get started is to quit talking and begin doing."),
            new Quote()
                    .id(3)
                    .quote("Your time is limited, so don't waste it living someone else's life."),
            new Quote()
                    .id(4)
                    .quote("If life were predictable it would cease to be life, and be without flavor."),
            new Quote()
                    .id(5)
                    .quote("If you set your goals ridiculously high and it's a failure, you will fail above everyone else's success.")
    );

    public List<Quote> getAllQuotes() {
        return QUOTES;
    }

    public Optional<Quote> getQuoteById(Integer id) {
        return Optional.ofNullable(QUOTES.stream().filter(quote -> quote.getId().equals(id)).findFirst().orElse(null));
    }
}

Next, I add the following QuotesService—which calls the QuotesRepository—to a newly-created services package.

@RequiredArgsConstructor
@ApplicationScoped
public class QuotesService {
    private final QuotesRepository quotesRepository;

    public List<Quote> getAllQuotes() {
        return quotesRepository.getAllQuotes();
    }

    public Optional<Quote> getQuoteById(Integer id) {
        return quotesRepository.getQuoteById(id);
    }

    public Quote getRandomQuote() {
        List<Quote> quotes = quotesRepository.getAllQuotes();
        return quotes.get(ThreadLocalRandom.current().nextInt(quotes.size()));
    }
}

Finally, I implement the QuotesApi created by our API-First approach. I create a QuotesApiImpl class in a newly-created controllers package with the following contents:

@RequiredArgsConstructor
public class QuotesApiImpl implements QuotesApi {
    private final QuotesService quotesService;

    @Override
    public List<Quote> getAllQuotes() {
        return quotesService.getAllQuotes();
    }

    @Override
    public Quote getQuoteById(Integer id) {
        return quotesService.getQuoteById(id)
                .orElseThrow(() -> new NotFoundException("Quote not found for id: " + id));
    }

    @Override
    public Quote getRandomQuote() {
        return quotesService.getRandomQuote();
    }
}

We can add a controller test by creating the QuotesApiResourceTest in a newly-created controllers test package:

@QuarkusTest
class QuotesApiResourceTest {
    @Test
    void testGetAllQuotes() {
        given()
                .when().get("/quotes")
                .then()
                .statusCode(200)
                .contentType(ContentType.JSON)
                .body("$.size()", is(5));
    }

    @Test
    void testGetQuoteById() {
        given()
                .when().get("/quotes/1")
                .then()
                .statusCode(200)
                .contentType(ContentType.JSON)
                .body("id", is(1));
    }

    @Test
    void testGetRandomQuote() {
        given()
                .when().get("/quotes/random")
                .then()
                .statusCode(200)
                .contentType(ContentType.JSON)
                .body("id", isA(Integer.class));
    }
}

We also add the QuotesApiResourceIT integration test, which simply calls the test above:

@QuarkusIntegrationTest
class QuotesApiResourceIT extends QuotesApiResourceTest {
    // Integration Tests ran in packaged mode
}

Now we are ready to run our service.

Building and Running the Service

We use the following command to start a local instance of our API:

$ quarkus dev

The command builds the project, runs the tests, and starts a local instance:

Listening for transport dt_socket at address: 5005
                   _
  __ _ _   _  ___ | |_ ___  ___
 / _` | | | |/ _ \| __/ _ \/ __|
| (_| | |_| | (_) | ||  __/\__ \
 \__, |\__,_|\___/ \__\___||___/
    |_|

       Powered by Quarkus 3.19.4
2025-03-30 12:51:38,235 INFO  [io.quarkus] (Quarkus Main Thread) quotes-quarkus 1.0.0-SNAPSHOT on JVM (powered by Quarkus 3.19.4) started in 3.729s. Listening on: http://localhost:8080

2025-03-30 12:51:38,259 INFO  [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2025-03-30 12:51:38,261 INFO  [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, resteasy, resteasy-jackson, smallrye-context-propagation, smallrye-openapi, swagger-ui, 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>

We can validate that the service is working locally with the following curl command:

curl --location 'http://localhost:8080/quotes/random'
{
    "id": 2,
    "quote": "The way to get started is to quit talking and begin doing."
}

Everything looks good!

The Dev UI in Quarkus

When running in local dev mode, Quarkus provides a really nice developer UI:

The Endpoints option provides both RESTful URLs and additional URLs to help ease the development process:

We can also use the http://localhost:8080/q/swagger-ui/ URL to view the Swagger UI:

We can stop the local instance with Ctrl-C.

Leveraging Heroku to Deploy the Service

Since I used Heroku for my prior articles, I wonder if support exists for Quarkus services. It turns out… it does! Going with Heroku helps me deploy my services quickly. I don’t lose time dealing with infrastructure concerns.

Like before, I need to allow for the service port to be overridden. In this case, we just need to update the following line in application.properties, as PORT will be set by Heroku at runtime:

quarkus.http.port=${PORT:8080}

To match the Java version we’re using, we create a system.properties file in the root folder of the project. The file has one line:

java.runtime.version = 17

Then, I create a Procfile in the same location for customizing the deployment behavior. This file also has one line:

web: java \$JAVA_OPTS -jar target/quarkus-app/quarkus-run.jar

Next we need to execute the mvn package command as a final step before we attempt to deploy to Heroku. Expanding the target folder in IntelliJ shows the quarkus-run.jar file that is expected by the Procfile (above).

It’s time to deploy. With the Heroku CLI, I can deploy the service using a few simple commands. First, I authenticate the CLI and then create a new Heroku app.

$ heroku login 
$ heroku create

The CLI responds with the following response:

Creating app... done, ⬢ murmuring-refuge-95709
https://murmuring-refuge-95709-a8582dfe9b2b.herokuapp.com/ | https://git.heroku.com/murmuring-refuge-95709.git

My Heroku app instance is named murmuring-refuge-95709, so my service will run at https://murmuring-refuge-95709-a8582dfe9b2b.herokuapp.com/.

One last thing to do … push the code to Heroku, which deploys the service:

$ git push heroku main

Switching back to Heroku Dashboard, we see our service has deployed successfully:

But Wait … There’s More

In addition to the JAR-based approach, Heroku supports deploying Quarkus services using a Docker or Podman container. Taking this approach provides complete control over its content, allowing deployment via a native executable running on GraalVM.

See this guide for additional details.

Motivational Quotes in Action

Using the Heroku app URL, https://murmuring-refuge-95709-a8582dfe9b2b.herokuapp.com/, we can now test our Motivational Quotes API using curl commands.

First, we retrieve the list of quotes:

curl --location 'https://murmuring-refuge-95709-a8582dfe9b2b.herokuapp.com/quotes'
[
    {
        "id": 1,
        "quote": "The greatest glory in living lies not in never falling, but in rising every time we fall."
    },
    {
        "id": 2,
        "quote": "The way to get started is to quit talking and begin doing."
    },
    {
        "id": 3,
        "quote": "Your time is limited, so don't waste it living someone else's life."
    },
    {
        "id": 4,
        "quote": "If life were predictable it would cease to be life, and be without flavor."
    },
    {
        "id": 5,
        "quote": "If you set your goals ridiculously high and it's a failure, you will fail above everyone else's success."
    }
]

We can retrieve a single quote by its ID:

curl --location 'https://murmuring-refuge-95709-a8582dfe9b2b.herokuapp.com/quotes/3'
{
    "id": 3,
    "quote": "Your time is limited, so don't waste it living someone else's life."
}

We can retrieve a random motivational quote:

curl --location 'https://murmuring-refuge-95709-a8582dfe9b2b.herokuapp.com/quotes/random'
{
    "id": 2,
    "quote": "The way to get started is to quit talking and begin doing."
}

We can view the Heroku Dashboard again to see our metrics after running these commands:

Conclusion

My readers may recall my personal mission statement, which I feel can apply to any IT professional:

“Focus your time on delivering features/functionality that extends the value of your intellectual property. Leverage frameworks, products, and services for everything else.” — J. Vester

In this article, I had to step outside my comfort zone and work with Quarkus for the very first time. I was able to ease the learning curve by leveraging ChatGPT. Once the project was created and I saw the build system being used was Maven, I was able to use my existing skills to get the API-First functionality in place.

From there, I was able to reach out to ChatGPT again to ask how I could convert my existing Spring Boot code to work with Quarkus. The ensuing response introduced me to the @ApplicationScoped annotation used by Quarkus. Quarkus provided a CLI that helped me build and run the service locally, while also providing a Dev UI to ease the process to learn this new service option.

From a Heroku side, I was able to quickly deploy and validate my service using the same approach I have followed before. This saved me time in trying to figure out something new when my preference is to focus on making my service better.

For these reasons, ChatGPT, Quarkus, and Heroku all adhere to my mission statement. They helped me deploy the new API quickly and without a huge learning burden.

If you are interested in the source code for this article, it is available on GitLab.

Have a really great day!

0
Subscribe to my newsletter

Read articles from John Vester directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

John Vester
John Vester

Information Technology professional with 30+ years expertise in application design and architecture, feature development, project management, system administration and team supervision. Currently focusing on enterprise architecture/application design utilizing object-oriented programming languages and frameworks.