These tutorials target Micronaut Framework 3. Read, Guides for Micronaut Framework 4.

Use OpenAPI Definition to Generate a Micronaut Client

Learn how to generate a Declarative Micronaut Client API from an OpenAPI definition and how to use it in your application

Authors: Andriy Dmytruk

Micronaut Version: 3.9.2

1. Getting Started

OpenAPI is a specification for describing REST APIs. Many web APIs provide definitions for the available endpoints and parameters in form of OpenAPI documents. OpenAPI infrastructure includes numerous tooling, including the OpenAPI generator.

OpenAPI Generator allows generation of API client libraries (SDK generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (both 2.0 and 3.0 are supported).

In this guide, we will use the OpenAPI Generator to generate a client API from an OpenAPI definition file. We will generate the client for Twitter API, however the steps could easily be replicated with any other API with an OpenAPI definition.

After generation, we will see how to use the client inside a Micronaut application, including performing API calls in controller methods.

2. What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.8 or greater installed with JAVA_HOME configured appropriately

3. Solution

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.

4. Installing OpenAPI Generator

To use OpenAPI Generator, we need to install OpenAPI Generator CLI.

We will use the jar option. Download it and store it in the directory that you want to use for this project.

Next, open terminal in the same directory. To verify that generator works correctly run the help command:

java -jar openapi-generator-cli-XXX.jar help

It should provide a description and a list of commands.

All the options for installation are given at the "CLI Installation" guide on the OpenAPI Generator website.

In particular:

  • run brew install openapi-generator for Homebrew installation,

  • or read the "Bash Launcher Script" section to set up a bash script with automatic updates

If you installed the generator with a package manager or bash launcher script, simply run

openapi-generator-cli help
You can also use the OpenAPI Generator Online Service but its usage is not covered by this guide.

5. Generating the Micronaut Client

We will generate a client for Twitter API.

To do this, we will need to download the official Twitter API OpenAPI definition file: https://api.twitter.com/2/openapi.json. Save it as twitter-api-definition.json and move into the directory that you intend to use for this project.

To generate the Micronaut client from the JSON definition, open the terminal in the directory where the definition file and the OpenAPI generator executable are located, and run:

java -jar openapi-generator-cli-5.4.0.jar generate \
    -g java-micronaut-client \(1)
    -i twitter-api-definition.json \(2)
    -o ./ \(3)
    -p apiPackage=example.micronaut.api \(4)
    -p modelPackage=example.micronaut.model \(5)
    -p build=gradle \(6)
    -p test=junit(7)
1 Specify that we will use the Java Micronaut client generator.
2 Specify our OpenAPI definition file as twitter-api-definition.json.
3 Specify the output directory to be the current directory (./). You can specify it to be a different one if you want.
4 We provide generator-specific properties starting with -p. We want all the API files to be generated in the example.twitter.api package.
5 We want all the models to be in the example.twitter.model package. In this package classes for data models, that are used for communication with server will be generated.
6 We want to use gradle as build tool. The supported values are gradle, maven and all. If nothing is specified, both Maven and Gradle files are generated.
7 We want to use JUnit 5 for testing. The supported values are junit (JUnit 5) and spock. If nothing is specified, junit is used by default.

After generation finishes, you should see the following directory structure:

./
├── docs
│   └── ... (1)
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── example/twitter/
│   │   │       ├── api (2)
│   │   │       │   ├── GeneralApi.java
│   │   │       │   ├── SpacesApi.java
│   │   │       │   ├── TweetsApi.java
│   │   │       │   └── UsersApi.java
│   │   │       └── model (3)
│   │   │           ├── ...
│   │   │           ├── Tweet.java
│   │   │           ├── TweetSearchResponse.java
│   │   │           ├── User.java
│   │   │           └── ...
│   │   └── resources/
│   │       ├── application.yml (4)
│   │       └── logback.xml
│   └── test/
│       └── java/
│           └── example/twitter/ (5)
│               ├── api
│               │   ├── GeneralApiTest.java
│               │   ├── SpacesApiTest.java
│               │   ├── TweetsApiTest.java
│               │   └── UsersApiTest.java
│               └── model
│                   └── ...
├── README.md
└── ...
1 The docs/ directory contains automatically generated Markdown files with documentation about your API.
2 The example.twitter.api package contains API classes.
3 The example.twitter.model package contains data classes, that are used in parameters or bodies of both requests and responses in the API.
4 The application.yml file contains the configuration of your Micronaut application. We will modify it later.
5 Templates for tests are generated. Test files for both APIs and models were created.

The definition file is a document describing the Twitter API according to the OpenAPI Specification.

If you want to learn about the structure of OpenAPI specification and how to simplify the creation of a Micronaut Server with it, read the "Use OpenAPI Defintion to Generate a Micronaut Server" guide or the OpenAPI guide.

As you can see, four API files were generated for the Twitter API.

If you look inside the API files, you will see that method definitions were generated corresponding to different paths and operations available in the API. Using Micronaut`s features, we will be able to inject a client implementation of this interface and use it by calling the corresponding methods without the need to worry how client-server communication is handled.

If you want to view all the available parameters for micronaut server generator, run

java -jar openapi-generator-cli-5.4.0.jar config-help \
-g java-micronaut-client

6. Configure Client Authorization

In order to send requests to an API, you might be required to configure Authorization.

6.1. Create Twitter Development Account

In case of Twitter, we need to configure OAuth2 authorization. You can authorize with an API key via the OAuth2 authorization code flow or using a Bearer token.

We will use the API key option. To get your API key, you will need to sign up for a Twitter developer account. To do this, follow the first 2 steps on the "Getting Access to the Twitter API" guide. Do not forget to save your API key and API secret values somewhere.

You can read about Twitter OAuth2 authentication in the "Authorization" page of the Twitter API documentation. The documentation specifies authentication with Bearer token and using an API key.

If you forgot to save your API key and API secret, you can get a new pair:

  • on the Twitter developer portal, open your project settings,

  • inside project settings, open application settings,

  • find the Keys and tokens tab,

  • inside the tab find the API key and secret section and click Regenerate to retrieve a new pair of keys.

6.2. Configure Authorization for Micronaut Client

We will modify the application.yml to include the following configuration:

src/main/resources/application.yml
micronaut:
  application:
    name: openapi-micronaut
  server:
    port: 8080
  security:
    oauth2:
      clients:
        twitter: (1)
          client-id: '${TWITTER_AUTH_CLIENT_ID:}' (4)
          client-secret: '${TWITTER_AUTH_CLIENT_SECRET:}'
          client-credentials:
            service-id-regex: '.*'
            uri-regex: 'api.twitter.com/.*' (5)
          token:
            url: "${openapi-micronaut-client-base-path}oauth2/token" (3)
            auth-method: "client_secret_basic" (2)
1 We add a value to the miconaut.security.oauth2.clients. We will add it with the key of twitter.
2 Set the authorization flow to client_secret_basic. This authorization was created for applications that need to access the resources on behalf of itself and requires an id and a secret parameter, and a token url.
3 Specify the url for retrieving tokens. We found it in the Twitter documentation.
4 Specify the client-id and client-secret parameters. You could directly put your id and secret directly as values in the configuration, however this highly discouraged due to security concerns. We will point to environment variables instead.
5 Specify that this authorization should be used for all requests to the Twitter api URL (starting with api.twitter.com). Also, set it to be used for all service ids.

Before running the application we will need to set the values of client-id and client-secret using environment variables. To do it, paste your API key and API secret into the following command and run in terminal:

export TWITTER_AUTH_CLIENT_ID=XXXXXXXXXXXXXXXXXXXXXXXXX
export TWITTER_AUTH_CLIENT_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Do not forget to add these lines before running any tests or application. If you are using an IDE, there probably should be an option to add environment variables in the run settings. View the documentation of your specific IDE for details.

Now, the client should authorize correctly, and we will be able to send requests.

7. Testing the Client

You can see that four APIs were generated in the project. These APIs have paths logically distributed between them. You can look at Twitter API v2 documentation for description on each of the paths, and notice that the sections are also logically split into Tweets, Users and Spaces, as well.

We will show how we can use the Twitter API by writing some simple tests using the generated Micronaut client. First, we will use the recent tweet counts API to get the number of tweets about Toronto in the last 7 days. Open the TweetsApiTest and rewrite the contents of the file with the following:

src/test/java/example/twitter/api/TweetsApiTest.java
package example.twitter.api;

import example.twitter.model.SearchCount;
import example.twitter.model.Tweet;
import example.twitter.model.TweetCountsResponse;
import example.twitter.model.TweetSearchResponse;
import example.twitter.model.User;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;

import java.time.OffsetDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;


/**
 * API tests for TweetsApi
 */
@MicronautTest (1)
@EnabledIfEnvironmentVariable(named = "TWITTER_AUTH_CLIENT_ID", matches = ".+") (2)
public class TweetsApiTest {

    @Inject
    TweetsApi api; (3)

    /**
     * Recent search counts
     */
    @Test
    public void tweetCountsRecentSearchTest() {
        // WHEN
        String query = "Toronto";
        TweetCountsResponse response = api.tweetCountsRecentSearch(
                query, null, null, null, null, null, null).block(); (4)

        // THEN
        assertNotNull(response);
        // Calculate total count
        assertNotNull(response.getData());
        Integer totalCount = response.getData().stream()
                .filter(Objects::nonNull)
                .map(SearchCount::getTweetCount)
                .reduce(0, Integer::sum);
        // There should be more than 100 tweets with such query in the last 7 days
        assertTrue(totalCount > 100); (5)
    }

}
1 Annotating the class with @MicronautTest will create a Micronaut context and allows utilizing various Micronaut features, like injection. More info.
2 Enable the test only if Twitter developer credentials are present.
3 Injection for TweetsApi.
4 Use the api to send a request to an API path for counting tweets with a specific query. We will count tweets that mentioned "Toronto".
5 We will verify that the data is present. Using manipulations with the generated data models we will calculate the total number of tweets and verify that there are at least 100 (which reasonably should be always true).

Now, we will test receiving the latest 10 tweets about New York City (NYC). We will use the recent tweets lookup API. To test the data models, we will get multiple properties of the tweet, and also properties of the user who posted it. We will add the following test to the TwetsApiTest class:

src/test/java/example/twitter/api/TweetsApiTest.java
    /**
     * Recent search
     */
    @Test
    public void tweetsRecentSearchTest() {
        // GIVEN
        String query = "nyc"; (1)
        OffsetDateTime startTime = null;
        OffsetDateTime endTime = null;
        String sinceId = null;
        String untilId = null;
        Integer maxResults = 10;
        String nextToken = null;
        Set<String> expansions = new HashSet<>(Collections.singletonList("author_id")); (2)
        Set<String> tweetFields = new HashSet<>(Arrays.asList( (3)
                "id", "author_id", "created_at", "attachments", "lang", "possibly_sensitive", "text", "source"
        ));
        Set<String> userFields = new HashSet<>(Arrays.asList("description", "created_at", "username", "name")); (4)
        Set<String> mediaFields = null;
        Set<String> placeFields = null;
        Set<String> pollFields = null;

        // WHEN
        TweetSearchResponse response = api.tweetsRecentSearch(
                query, startTime, endTime, sinceId, untilId, maxResults, nextToken, expansions, tweetFields,
                userFields, mediaFields, placeFields, pollFields).block(); (5)

        // THEN
        assertNotNull(response);

        // Tweets should be present
        List<Tweet> tweets = response.getData(); (6)
        assertNotNull(tweets);
        assertEquals(maxResults, tweets.size());

        Tweet tweet = tweets.get(0);
        assertNotNull(tweet);
        assertNotNull(tweet.getAuthorId());
        assertNotNull(tweet.getCreatedAt());
        assertNotNull(tweet.getLang());
        assertNotNull(tweet.getPossiblySensitive());
        assertNotNull(tweet.getText());

        // Users should be present
        assertNotNull(response.getIncludes());
        List<User> users = response.getIncludes().getUsers(); (7)
        assertNotNull(users);
        assertTrue(users.size() > 0);

        User user = users.get(0);
        assertNotNull(user);
        assertNotNull(user.getId());
        assertNotNull(user.getUsername());
    }
1 Set the search query to "nyc".
2 Expand the query on the author_id property to additionally get information about the user based on their id. You can read about expanding in the Twitter API expansions documentation page.
3 Specify the fields we want to get for each tweet.
4 Specify the fields we want to get for users (authors of tweets).
5 Retrieve the response from server.
6 Get all the tweets from the response and verify that some common properties are present.
7 Get users form a separate property of the response and verify that the information is present.

Finally, we will test getting multiple pages using the same recent tweets lookup API:

src/test/java/example/twitter/api/TweetsApiTest.java
    /**
     * Recent search with the nextToken parameter set (to retrieve batch)
     */
    @Test
    public void tweetsRecentSearchTestNextToken() {
        // WHEN
        String query = "Toronto";
        int maxResults = 10;
        TweetSearchResponse response = api.tweetsRecentSearch(query, null, null, null,
                null, maxResults, null, null, null, null, null, null, null).block(); (1)

        // THEN
        assertNotNull(response);
        assertNotNull(response.getData());
        assertEquals(maxResults, response.getData().size());
        Set<String> tweetIds = response.getData().stream().map(Tweet::getId).collect(Collectors.toSet());

        assertNotNull(response.getMeta());
        String nextToken = response.getMeta().getNextToken();
        assertNotNull(nextToken);

        // WHEN
        TweetSearchResponse nextResponse = api.tweetsRecentSearch(query, null, null, null,
                null, maxResults, nextToken, null, null, null, null,
                null, null).block(); (2)

        // THEN
        assertNotNull(nextResponse);
        assertNotNull(nextResponse.getData());
        assertEquals(maxResults, nextResponse.getData().size());
        // Calculate the intersection and verify that no match
        Set<String> nextTweetIds = nextResponse.getData().stream().map(Tweet::getId).collect(Collectors.toSet());
        nextTweetIds.retainAll(tweetIds);
        assertTrue(nextTweetIds.isEmpty()); (3)
    }
1 Retrieve the first 10 tweets that match our query.
2 Retrieve the next 10 tweets that match the query using the nextToken parameter.
3 Verify that the no tweets on the two pages match.

We will now proceed to use the API in a simple application. However, you would probably want to also implement tests for all other paths of the API, if you were planning to use them in your application.

To run the tests:

./gradlew test

Then open build/reports/tests/test/index.html in a browser to see the results.

All the tests should run successfully.

8. Using Client in a Controller

We will now write a controller, that will search for most frequent words in the latest tweets on a given topic.

8.1. Creating an Introspected Class

We will first define an object to represent the response. Create a file for WordFrequency class, that will have the word itself, and the number it occurred as properties:

src/main/java/example/micronaut/model/WordFrequency.java
package example.micronaut.model;

import io.micronaut.core.annotation.Introspected;
import io.micronaut.core.annotation.NonNull;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Introspected (1)
public class WordFrequency {
    @NonNull @NotBlank
    private final String word;
    @NonNull @NotNull
    private final Integer numberOccurred; (2)

    public WordFrequency(@NonNull String word, @NonNull Integer numberOccurred) {
        this.word = word;
        this.numberOccurred = numberOccurred;
    }

    @NonNull
    public String getWord() {
        return word;
    }

    @NonNull
    public Integer getNumberOccurred() {
        return numberOccurred;
    }
}
1 Annotate the class with @Introspected to generate BeanIntrospection metadata at compilation time. This information can be used, for example, to render the POJO as JSON using Jackson without using reflection.
2 Define the two required properties and create getters for them.

8.2. Implementing the Controller

We will create the API of the controller. It will use the generated TweetApi client, and the WordFrequency class, which we just created. Create a file for TwitterFrequentWordsController class and paste the following:

src/main/java/example/micronaut/controller/TwitterFrequentWordsController.java
package example.micronaut.controller;

import example.twitter.api.TweetsApi;
import example.twitter.model.TweetSearchResponse;
import example.micronaut.model.WordFrequency;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.QueryValue;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.rules.SecurityRule;
import reactor.core.publisher.Flux;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Controller("/twitter-frequent-words") (1)
public class TwitterFrequentWordsController {

    public static int TWEETS_NUMBER = 50;

    private final TweetsApi tweetsApi;

    public TwitterFrequentWordsController(TweetsApi tweetsApi) { (2)
        this.tweetsApi = tweetsApi;
    }

    @Get (3)
    @Secured(SecurityRule.IS_ANONYMOUS)
    public Flux<WordFrequency> get(@QueryValue("search-query") String searchQuery,
                                   @QueryValue("words-n") Integer wordsNumber (4)
    ) {
        return tweetsApi.tweetsRecentSearch(searchQuery,
                null, null, null, null, TWEETS_NUMBER, null, null,
                null, null, null, null, null)
                .flatMapIterable(v -> getTopFrequentWords(v, wordsNumber)); (5)
    }

}
1 The class is defined as a controller with the @Controller annotation mapped to the path /twitter-frequent-words.
2 Injection for TweetsApi.
3 The @Get annotation maps the get method to an HTTP GET request on /twitter-frequent-words.
4 Use the @QueryValue annotation to define the search-query and words-n parameters to be provided in the query part of the URL. Micronaut will parse them from request and provide as arguments to the method.
5 Make a request to the Twitter api and process the data using a function that we will implement next.

Now add the method responsible for processing the data to the controller. There is no need to get into the details of its implementation, but we will leave some description for a curious reader:

src/main/java/example/micronaut/controller/TwitterFrequentWordsController.java
    @NonNull
    private static Map<String, Integer> getWordOccurrences(@NonNull TweetSearchResponse response) { (1)
        Map<String, Integer> wordOccurrences = new HashMap<>(); (2)

        response.getData().forEach(tweet ->
                Arrays.stream(tweet.getText().split("[^a-zA-Z]+"))
                        .map(String::toLowerCase)
                        .filter(w -> w.length() > 3)
                        .forEach(word -> wordOccurrences.put(word, wordOccurrences.getOrDefault(word, 0) + 1)) (3)
        );

        return wordOccurrences;
    }

    @NonNull
    private static List<WordFrequency> getTopFrequentWords(@NonNull TweetSearchResponse response,
                                                           @NonNull Integer wordsNumber) {
        if (response.getData() == null) {
            return Collections.emptyList();
        }

        return getWordOccurrences(response).entrySet().stream()
                .sorted(Map.Entry.comparingByValue(TwitterFrequentWordsController::compareIntegersReversed))
                .limit(wordsNumber)
                .map(v -> new WordFrequency(v.getKey(), v.getValue()))
                .collect(Collectors.toList()); (4)
    }

    private static Integer compareIntegersReversed(Integer v1, Integer v2) {
        return v2 - v1;
    }
1 Create a method to calculate the number of occurrences for all the words.
2 Create a Map to store the number of occurrences.
3 Split the text of tweets on words (we will split by any non-alphabetic character), filter out all words shorter than four characters to remove articles and increment the counter for each word in the Map.
4 Sort the words by the number of occurrences, limit the number by certain amount, and convert the result to a list with instances of WordFrequency.

9. Running the Application

To run the application, use the ./gradlew run command, which starts the application on port 8080.

You can send a few requests to the path to test the application. We will use cURL for that.

  • The search for 20 most frequent words in tweets about "Ukraine" returns words representing relevant topics at the time (beginning of 2022):

    curl -i 'localhost:8080/twitter-frequent-words?words-n=20&search-query=Ukraine'
    [
      {"word": "ukraine","numberOccurred": 40}, {"word": "https",    "numberOccurred": 15},
      {"word": "russian","numberOccurred": 12}, {"word": "with",     "numberOccurred": 9},
      {"word": "russia", "numberOccurred": 9},  {"word": "invasion", "numberOccurred": 7},
      {"word": "from",   "numberOccurred": 6},  {"word": "india",    "numberOccurred": 5},
      {"word": "that",   "numberOccurred": 5},  {"word": "about",    "numberOccurred": 5},
      {"word": "border", "numberOccurred": 4},  {"word": "army",     "numberOccurred": 4},
      {"word": "putin",  "numberOccurred": 4},  {"word": "people",   "numberOccurred": 4},
      {"word": "forces", "numberOccurred": 4},  {"word": "troops",   "numberOccurred": 4},
      {"word": "their",  "numberOccurred": 4},  {"word": "will",     "numberOccurred": 4},
      {"word": "biden",  "numberOccurred": 4},  {"word": "just",     "numberOccurred": 3}
    ]
  • The results of search for 10 most common words in tweets containing the substring "farm" also seem to be reasonable:

    curl -i 'localhost:8081/twitter-frequent-words?words-n=10&search-query=farm'
    [
      {"word": "farm",      "numberOccurred": 47}, {"word": "workers",   "numberOccurred": 17},
      {"word": "https",     "numberOccurred": 16}, {"word": "hello",     "numberOccurred": 10},
      {"word": "animal",    "numberOccurred": 9},  {"word": "redvelvet", "numberOccurred": 9},
      {"word": "those",     "numberOccurred": 7},  {"word": "their",     "numberOccurred": 7},
      {"word": "temporary", "numberOccurred": 6},  {"word": "revi",      "numberOccurred": 6}
    ]

It is also highly recommended, that you implement tests for all the controllers you write, but we will skip writing tests for our controller in this guide.

10. Generate a Micronaut Application Native Executable with GraalVM

We will use GraalVM, the polyglot embeddable virtual machine, to generate a native executable of our Micronaut application.

Compiling native executables ahead of time with GraalVM improves startup time and reduces the memory footprint of JVM-based applications.

Only Java and Kotlin projects support using GraalVM’s native-image tool. Groovy relies heavily on reflection, which is only partially supported by GraalVM.

10.1. Native executable generation

The easiest way to install GraalVM on Linux or Mac is to use SDKMan.io.

Java 11
sdk install java 22.3.r11-grl
If you still use Java 8, use the JDK11 version of GraalVM.
Java 17
sdk install java 22.3.r17-grl

For installation on Windows, or for manual installation on Linux or Mac, see the GraalVM Getting Started documentation.

After installing GraalVM, install the native-image component, which is not installed by default:

gu install native-image

To generate a native executable using Gradle, run:

./gradlew nativeCompile

The native executable is created in build/native/nativeCompile directory and can be run with build/native/nativeCompile/micronautguide.

It is possible to customize the name of the native executable or pass additional parameters to GraalVM:

build.gradle
graalvmNative {
    binaries {
        main {
            imageName.set('mn-graalvm-application') (1)
            buildArgs.add('--verbose') (2)
        }
    }
}
1 The native executable name will now be mn-graalvm-application
2 It is possible to pass extra arguments to build the native executable

11. Next Steps

11.1. Learn How to Write OpenAPI Definition and Generate Server Based on It

  • understand OpenAPI definition files and write your own definition files,

  • generate server API based on the definitions,

  • implement the functionality of the server based on the API and write comprehensive tests utilizing Micronaut`s features.

11.2. Learn Micronaut

To learn more about Micronaut framework and its features visit Micronaut documentation or read one of the several Micronaut guides.

11.3. Micronaut OpenAPI

  • Use Micronaut OpenAPI module to generate OpenAPI definition documents from controllers with Micronaut annotations.