How To Introduce a New API Quickly Using Micronaut

John VesterJohn Vester
11 min read

In the first two articles of this series (part 1 and part 2), I demonstrated how quickly an idea can become a reality using Spring Boot, the framework I have used for over 10 years to establish new services. I stepped out of my comfort zone in the last article (part 3) when I used Quarkus for the first time, which offered a really nice CLI to assist with the development process.

I would like to close out this short series with another framework that’s new (to me), called Micronaut.

Micronaut is an open-source JDK-based framework focused on lightweight microservices. Under the hood, Micronaut does not rely on reflective programming, but emphasizes an inversion of control (IoC) design which results in less memory usage and a much faster start time. A robust CLI exists too.

While the service should start fast, the biggest question I have is: “How quickly can I transform my motivation quotes idea to a reality using Micronaut?

Getting Started with Micronaut

As noted above, just like with Spring Boot and Quarkus, Micronaut also has a CLI (mn). There is also an initializer (called a launcher), which is very similar to what Spring Boot offers. For this article, we’ll use the CLI, and I’ll use Homebrew to perform the install:

$ brew install --cask micronaut-projects/tap/micronaut

Other installation options can be found here.

To get started, we’ll issue the mn command:

$ mn

For MacOS users, you will likely need to visit the Privacy & Security section in System Settings to allow the use of the mn command.

We’ll simply use create to interactively initialize a new service:

mn> create

Apr 01, 2025 2:52:20 PM org.jline.utils.Log logr
WARNING: Unable to create a system terminal, creating a dumb terminal (enable debug logging for more information)
What type of application do you want to create? (enter for default)
*1) Micronaut Application
 2) Micronaut CLI Application
 3) Micronaut Serverless Function
 4) Micronaut gRPC Application
 5) Micronaut Messaging Application

We’ll select the default option to create a new application.

Choose your preferred language. (enter for default)
*1) Java
 2) Groovy
 3) Kotlin

We’ll use Java for this exercise.

Choose your preferred test framework. (enter for default)
*1) JUnit
 2) Spock
 3) Kotest

We’ll use JUnit for our test framework.

Choose your preferred build tool. (enter for default)
*1) Gradle (Groovy)
 2) Gradle (Kotlin)
 3) Maven

We’ll plan to use Gradle (Groovy) this time.

Choose the target JDK. (enter for default)
*1) 17
 2) 21

We’ll stick with Java 17, since it matches what we’ve used for other articles in this series.

Enter any features to apply. Use tab for autocomplete and separate by a space.

For now, we’ll add support for OpenAPI and Swagger and press the return key:

openapi swagger-ui
Enter a name for the project.

We’ll use the name quotes-micronaut for our project.

Now let’s exit out of mn using Control-C and check out the base directory structure:

$ cd quotes-micronaut && ls -la
total 88
drwxr-xr-x@ 13 johnvester   416 Apr  1 14:58 .
drwxrwxrwx  93 root        2976 Apr  1 14:58 ..
-rw-r--r--@  1 johnvester   127 Apr  1 14:58 .gitignore
-rw-r--r--@  1 johnvester  1380 Apr  1 14:58 README.md
-rw-r--r--@  1 johnvester  1590 Apr  1 14:58 build.gradle
drwxr-xr-x@  3 johnvester    96 Apr  1 14:58 gradle
-rw-r--r--@  1 johnvester    23 Apr  1 14:58 gradle.properties
-rwxr--r--@  1 johnvester  8762 Apr  1 14:58 gradlew
-rw-r--r--@  1 johnvester  2966 Apr  1 14:58 gradlew.bat
-rw-r--r--@  1 johnvester   367 Apr  1 14:58 micronaut-cli.yml
-rw-r--r--@  1 johnvester   182 Apr  1 14:58 openapi.properties
-rw-r--r--@  1 johnvester    39 Apr  1 14:58 settings.gradle
drwxr-xr-x@  4 johnvester   128 Apr  1 14:58 src

We can open the project in IntelliJ and run the Application class:

 __  __ _                                  _   
|  \/  (_) ___ _ __ ___  _ __   __ _ _   _| |_ 
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| |  | | | (__| | | (_) | | | | (_| | |_| | |_ 
|_|  |_|_|\___|_|  \___/|_| |_|\__,_|\__,_|\__|
15:01:56.083 [main] INFO  io.micronaut.runtime.Micronaut - 
Startup completed in 648ms. Server Running: http://localhost:8080

We can validate that the RESTful API is working, too, by calling the URI configured in the QuotesMicronautController class.

@Controller("/quotes-micronaut")
public class QuotesMicronautController {
    @Get(uri="/", produces="text/plain")
    public String index() {
        return "Example Response";
    }
}

We’ll use the following cURL command:

curl --location 'http://localhost:8080/quotes-micronaut'

This returns the following text:

Example Response

The only difference I saw with using the CLI over the launcher is that I wasn’t able to specify a base package. That’s okay, because we can use the CLI to establish the rest of our classes:

$ mn create-bean quotes.micronaut.repositories.QuotesRepository
$ mn create-bean quotes.micronaut.services.QuotesService
$ mn create-bean quotes.micronaut.controllers.QuotesController

You’ll notice we are creating our own controller class. This is simply to follow the same approach I’ve used in my earlier articles. The auto-generated QuotesMicronautController would have worked just fine.

To make things less complicated for the injection side of things, we can use Lombok, adding the following dependencies to the build.gradle file:

compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")

Minor Roadblock with OpenAPI Generation

When I looked up the server-side OpenAPI generators, I was excited to see java-micronaut-server as an option. Upon further review, the documentation indicated this has a stability level of “BETA.”

The beta version was able to generate the expected Quote model object, but I ran into issues trying to generate an interface or abstract class that my controllers could extend. So I decided to create an issue in the openapi-generator library (link for those who are interested in the details) and pivot toward another approach.

Using Cursor AI to Convert My Quarkus Service

While sharing this experience with Alvin Lee (a longtime colleague), he had an idea. Alvin asked, “What if we use Cursor AI to analyze your Quarkus repo and port it to Micronaut for us?”

I told Alvin to “go for it” and became excited, because this article really had not required the use of AI like the other articles in the series.

I gave Alvin access to fork my Quarkus repo and within a matter of minutes, the service was completely ported to Micronaut. Let’s quickly review the classes that were created automatically.

First, let’s review the repository layer:

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

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

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

Next, let’s take a look at the service layer:

@Singleton
public class QuotesService {
  private final QuotesRepository quotesRepository;

  @Inject
  public QuotesService(QuotesRepository quotesRepository) {
    this.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, we can examine the controller that will be the consumer-facing interface to our API:

@Controller("/quotes")
public class QuotesController {
  @Inject
  private QuotesService quotesService;

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

  @Get("/{id}")
  public Optional<Quote> getQuoteById(@PathVariable Integer id) {
    return quotesService.getQuoteById(id);
  }

  @Get("/random")
  public Quote getRandomQuote() {
    return quotesService.getRandomQuote();
  }
}

Validating Our Service Locally

To validate the service locally, we’ll start the service from IntelliJ like we did before:

 __  __ _                                  _   
|  \/  (_) ___ _ __ ___  _ __   __ _ _   _| |_ 
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| |  | | | (__| | | (_) | | | | (_| | |_| | |_ 
|_|  |_|_|\___|_|  \___/|_| |_|\__,_|\__,_|\__|
21:18:54.740 [main] INFO  io.micronaut.runtime.Micronaut - 
Startup completed in 284ms. Server Running: http://localhost:8080

With all the code in place, the Micronaut service started in just 284 milliseconds.

We’ll use the following cURL command to retrieve a random motivational quote:

curl --location 'http://localhost:8080/quotes/random'

I received a 200 OK HTTP response and the following JSON payload:

{
  "id": 3,
  "quote": "Your time is limited, so don't waste it living someone else's life."
}

Looks like the port was quick and successful! Now it is time to deploy.

Leveraging Heroku to Deploy the Service

Since I used Heroku for my prior articles, I wondered if support existed for Micronaut services.

Although Heroku recently published some updated information regarding Micronaut support in its Java buildpack, I felt like I could use my existing experience to get things going. Selecting Heroku helps me deploy my services quickly, and I don’t lose time dealing with infrastructure concerns.

The first thing I needed to do was add the following line to the application.properties file to make sure the server’s port could be overridden by Heroku:

micronaut.server.port=${PORT:8080}

Next, I created a system.properties file to specify we are using Java version 17:

java.runtime.version = 17

Knowing that Heroku runs the “stage” Gradle task as part of the deployment, we can specify this in the manifest file we plan to use by adding the following lines to the build.gradle file:

jar {
  manifest {
    attributes(
      'Main-Class': 'quotes.micronaut.Application'
    )
  }
}

task stage(dependsOn: ['build', 'clean'])

build.mustRunAfter clean

Ordinarily, for running apps on Heroku, we would need to add a Procfile, where we specify the Heroku environment, reference the path to where the stage-based JARs will reside, and use the port set by Heroku. That file might look like this:

web: java $JAVA_OPTS -Dmicronaut.environments=heroku -Dserver.port=$PORT -jar build/libs/*.jar

However, on detecting a Micronaut app, the Heroku buildpack defaults to this:

java -Dmicronaut.server.port=$PORT $JAVA_OPTS -jar build/libs/*.jar

… and that's exactly what I need. No need for a Procfile after all! That's pretty slick.

Now we just need to login to Heroku and create a new application:

$ heroku login 
$ heroku create

The CLI responds with the following response:

Creating app... done, ⬢ aqueous-reef-26810
https://aqueous-reef-26810-4db1994daff4.herokuapp.com/ | 
https://git.heroku.com/aqueous-reef-26810.git

The Heroku app instance is named aqueous-reef-26810-4db1994daff4, so my service will run at https://aqueous-reef-26810-4db1994daff4.herokuapp.com/.

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

$ git push heroku main

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

We are now ready to give our service a try from the internet.

Motivational Quotes in Action

Using the Heroku app URL, https://aqueous-reef-26810-4db1994daff4.herokuapp.com/, we can now test our Motivational Quotes API using curl commands.

First, we retrieve the list of quotes:

curl --location 'https://aqueous-reef-26810-4db1994daff4.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://aqueous-reef-26810-4db1994daff4.herokuapp.com/quotes/4'
{
    "id": 4,
    "quote": "If life were predictable it would cease to be life, and be without flavor."
}

We can retrieve a random motivational quote:

curl --location 'https://aqueous-reef-26810-4db1994daff4.herokuapp.com/quotes/random'
{
    "id": 3,
    "quote": "Your time is limited, so don't waste it living someone else's life."
}

We can even view the auto-generated Swagger UI via Heroku:

Conclusion

In this article, I had to step outside my comfort zone again—this time working with Micronaut for the very first time. Along the way, I ran into an unexpected issue which caused me to pivot my approach to leverage AI—more specifically: Cursor. Once ready, I was able to use my existing knowledge to deploy the service to Heroku and validate everything was working as expected.

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

Micronaut offered a fully-functional CLI and a service which started the fastest of all the frameworks in the series. Cursor helped us stay on track by porting our Quarkus service to Micronaut. Like before, Heroku provided a fast and easy way to deploy the service without having to worry about any infrastructure concerns.

All three key solutions fully adhered to my mission statement, allowing me to not only be able to focus on my idea, but to make it an internet-accessible reality … quickly!

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.