Java / Gradle

Micronaut Server generation with OpenAPI

Learn how to write an OpenAPI definition, use it to generate a server template for a Micronaut application, and get it all to work

Andriy Dmytruk
On this guide
In this section

Getting Started

In this guide, we will write an OpenAPI definition file and then use it to generate a Java Micronaut server API with OpenAPI Generator.

Then, we will add internal logic to the API and test our implementation.

What OpenAPI Is

The OpenAPI Specification defines a format for uniquely describing REST APIs that is both human- and machine-readable. Later in this guide, we will discover the structure of documents in the OpenAPI format. We will also create such a document for our desired API. Note that we will refer to the document describing our API as an API definition file.

Advantages of OpenAPI

  • It provides a unique way of describing a REST API that is easy to understand and modify. It is the most broadly adopted industry standard for describing new APIs and has the most developed tooling ecosystem.

  • You can generate interactive documentation and client implementations from the same definition file in numerous languages.

  • You can use the same definition file to generate a server template. The template will include client-server communication specifics based on your API definition. It removes the need for developers to write extensive documentation about each possible path and parameter for the APIs - most can be described in the definition file. This prevents incompatibility issues between the client and server sides which might be caused by ill-communication.

    Note
    The internal server logic cannot be generated from a definition file and needs to be implemented manually based on the generated server template. The reason for this is very simple: there cannot be a unified way of describing all the possible server implementations.

What You Will Learn

  • You will discover the general structure of a document in the OpenAPI format and a definition file in this format describing the desired API for our custom server.

  • You will learn to use the Micronaut OpenAPI build tool plugins to generate Micronaut code in Java for the server application. We will extend the code by implementing internal logic and testing it.

  • You will learn how to use Micronaut Data JDBC to connect to a PostgreSQL database from our application to store and retrieve data. You will complement the application with tests.

Solution

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can directly get the complete solution by downloading and unzipping micronaut-openapi-generator-server-gradle-java.zip.

What you will need

To complete this guide, you will need the following:

Creating The API Definition File

We will now create a definition file that will describe our server API, including the available paths and operations.

The definition file must be in the OpenAPI format. The document must have a specific structure. "OpenAPI Specification" guide describes it with more detail. We will write sections of the definition document based on the specification.

OpenAPI generator supports .yml and .json file formats for the definition file. We will use YAML due to its simplicity and human readability.

In the directory where you downloaded the OpenAPI generator CLI, create a file named library-definition.yml and open it in your favourite text editor.

Describing General Server Info

We will first provide general server information in the definition file. Paste the following text to the file:

java/src/main/resources/library-definition.yml
Note
If you are new to OpenAPI, you might be interested in reading the OpenAPI guide or the OpenAPI 3.0.0 specification after you finish this guide.

Defining Paths and Operations

The paths section of the definition is described in the "API Endpoints" OpenAPI Guide, but can also be understood from a few examples. This section defines paths and various operations (like GET, PUT, and POST) available on these paths.

We will proceed by defining a path that is supposed to be used for searching books in our library. The parameters that we will define in the definition will be used to narrow the search results.

Paste the following to our file:

java/src/main/resources/library-definition.yml
Note
You can read more about parameter descriptions in the "Describing Parameters" OpenAPI guide. All the available types and their validations are described in "Data Models (Schemas)" OpenAPI guide.

We will define another path with a POST operation that is supposed to be used to add information about a book in our library. In this case, the request will contain a body with all the book information:

java/src/main/resources/library-definition.yml
Note
To read more about body definitions, see the "Describing Request Body" OpenAPI guide.

Defining Schemas

Schemas are required whenever a parameter, request body, or response body we want to describe needs to be an object. In that case, we add a schema that defines all the properties of the object. You can find out about the format for schemas in the "Content of Message Bodies" OpenAPI Guide.

We will add schemas to our definition file:

java/src/main/resources/library-definition.yml

As you can see, schemas can be defined as enums when they can only be assigned a finite number of values. Also, you can reference other schemas as properties of a schema.

Note
You can read more about writing schemas in the "Data Models (Schemas)" OpenAPI guide.

Save the file and proceed to the next part of the guide.

Writing the Application

Create an application using the Micronaut Command Line Interface or with Micronaut Launch.

mn create-app example.micronaut.micronautguide \
    --features=validation,security,reactor,data-jdbc,flyway,jdbc-hikari,postgres,graalvm \
    --build=gradle \
    --lang=java \
    --test=junit
Note
If you don’t specify the --build argument, Gradle with the Kotlin DSL is used as the build tool.
If you don’t specify the --lang argument, Java is used as the language.
If you don’t specify the --test argument, JUnit is used for Java and Kotlin, and Spock is used for Groovy.

The previous command creates a Micronaut application with the default package example.micronaut in a directory named micronautguide.

If you use Micronaut Launch, select Micronaut Application as application type and add validation, security, reactor, data-jdbc, flyway, jdbc-hikari, postgres, and graalvm features.

Note
If you have an existing Micronaut application and want to add the functionality described here, you can view the dependency and configuration changes from the specified features, and apply those changes to your application.

Generating Server API From The OpenAPI Definition

Now we will generate server API files from our definition. The generated server code will be in Java and will use the Micronaut features for client-server communication.

Open your build.gradle file and apply the micronaut-openapi plugin:

build.gradle
plugins {
  id 'io.micronaut.openapi' version '...'
  ....
}

And configure your build to generate a server:

build.gradle

The server code will be generated in your build directory but automatically added as a source set. Therefore, you can, for example, run ./gradlew compileJava --console=verbose and see that the sources are generated and compiled:

> Task :generateServerOpenApiApis
...
> Task :generateServerOpenApiModels
...
> Task :compileJava
Tip
The Micronaut OpenAPI generator supports a large number of parameters. Please refer to the Micronaut OpenAPI Gradle plugin documentation for all possible options.

After generation finishes, you should see the following directory structure under your build/generated/openapi directory:

Application Structure

To better understand the Micronaut Application we want to develop, let’s first look at the schematic of the whole application:

server component scheme
  • The controller will receive client requests utilizing Micronaut server features.

  • The controller will call repository methods responsible for interaction with the database.

  • The repository methods will be implemented utilizing Micronaut JDBC and will send queries to the database.

  • The files we generated with OpenAPI generator include Micronaut features responsible for server-client communication, like parameter and body binding, and JSON conversion.

Configuration

Set context-path to /.

java/src/main/resources/application.properties
context-path=/

Data Storage and Access with PostgreSQL and JDBC

We will use PostgreSQL database to store and access data. This will ensure that stored data is persistent between the server runs and can be easily accessed and modified by multiple instances of our application.

Before implementing any server logic, we need to create a database and configure a connection to it. We will use Flyway to set up the database schema and JDBC for accessing the data.

Configure Access for a Data Source

We will use Micronaut Data JDBC to access the PostgreSQL data source.

Add the following required dependencies:

build.gradle
annotationProcessor("io.micronaut.data:micronaut-data-processor")
implementation("io.micronaut.data:micronaut-data-jdbc")
implementation("io.micronaut.sql:micronaut-jdbc-hikari")
runtimeOnly("org.postgresql:postgresql")

Locally, the database will be provided by Micronaut Test Resources.

java/src/main/resources/application.properties

With the configured data source, you can access the data using the Micronaut JDBC API, which is shown later in the guide.

Database Migration with Flyway

We need a way to create the database schema. For that, we use Micronaut integration with Flyway.

Flyway automates schema changes, significantly simplifying schema management tasks, such as migrating, rolling back, and reproducing in multiple environments.

Add the following snippet to include the necessary dependencies:

build.gradle
implementation("io.micronaut.flyway:micronaut-flyway")

We will enable Flyway in the Micronaut configuration file and configure it to perform migrations on one of the defined data sources.

java/src/main/resources/application.properties
Note
Configuring multiple data sources is as simple as enabling Flyway for each one. You can also specify directories that will be used for migrating each data source. Review the Micronaut Flyway documentation for additional details.

Flyway migration will be automatically triggered before your Micronaut application starts. Flyway will read migration commands in the resources/db/migration/ directory, execute them if necessary, and verify that the configured data source is consistent with them.

Create the following migration files with the database schema creation:

java/src/main/resources/db/migration/V1__schema.sql
CREATE TYPE bookavailability as ENUM('available', 'reserved', 'not available');
CREATE cast ( character varying as bookavailability) WITH inout AS assignment;
CREATE TABLE book (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    availability bookavailability NOT NULL,
    author VARCHAR(255),
    ISBN CHAR(13)
);

INSERT INTO
    book(name, availability, author, ISBN)
VALUES
    ('Alice''s Adventures in Wonderland',      'available',   'Lewis Caroll',   '9783161484100'),
    ('The Hitchhiker''s Guide to the Galaxy',  'reserved',    'Douglas Adams',  NULL),
    ('Java Guide for Beginners',               'available',   NULL,             NULL);

The SQL commands in the migration will create the book table with id and four columns describing its properties, and populate the table with three sample rows.

Creating a MappedEntity

To retrieve objects from the database, you need to define a class annotated with @MappedEntity. Instances of the class will represent a single row retrieved from the database in a query.

We will now create BookEntity class. We will be retrieving data from the book table, and therefore class properties match columns in the table. Note that special annotations are added on the property corresponding to the primary key of the table.

java/src/main/java/example/micronaut/BookEntity.java

Writing a Repository

Next, we will create a repository interface and define the required operations to access the database. Micronaut Data will implement the interface at compilation time. It will determine the operations to be implemented based on method naming and parameters, and supports simple create, read, update, delete operations along with highly-customizable queries.

java/src/main/java/example/micronaut/BookRepository.java

Writing the Controller Logic

If you look inside the generated BookInfo.java file, you can see the class that was generated with all the parameters based on our definition. Notice that the constructor signature has two parameters, which were defined as required in the YAML definition file:

    public BookInfo(String name, BookAvailability availability) {

Along with that it has getters and setters for parameters and Jackson serialization annotations.

Create the Controller class

Micronaut OpenAPI has generated an interface called BooksApi that we need to implement in our controller.

Create a BooksController.java class with the following contents:

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

import example.micronaut.BookEntity;
import example.micronaut.BookRepository;
import example.micronaut.api.BooksApi;
import example.micronaut.model.BookInfo;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.annotation.Controller;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;

import java.util.List;

@Controller
public class BooksController implements BooksApi
 {

}

Implementing Controller Methods

Now open BooksController. Thanks to the @Controller annotation, an instance of the class will be initialized when Micronaut application starts, and the corresponding method will be called when there is a request. The class must implement the BooksApi interface: it should have two methods named the same as the operations we created in the definition file. The methods in the interface have Micronaut framework annotations describing the required API. We will now implement them in the controller.

Using the Inversion of Control principle, we will inject BookRepository so it can be used in the methods. When initializing the controller, Micronaut will automatically provide an instance of the repository as a constructor argument:

java/src/main/java/example/micronaut/controller/BooksController.java

Next, keeping all the generated annotations, add this implementation for the search method:

java/src/main/java/example/micronaut/controller/BooksController.java

Finally, we will implement the addBook method:

java/src/main/java/example/micronaut/controller/BooksController.java
@ExecuteOn(TaskExecutors.BLOCKING)
public void addBook(BookInfo bookInfo) {
    bookRepository.save(bookInfo.getName(), // 
            bookInfo.getAvailability(),
            bookInfo.getAuthor(),
            bookInfo.getIsbn());
}

Test Resources

When the application is started locally, either under test or while running locally, resolution of the datasource URL is detected and the Test Resources service will start a local PostgreSQL docker container, and inject the properties required to use this as the datasource.

For more information, see the JDBC section of the Test Resources documentation.

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 paths to test the application. We will use cURL for that.

  • The search for book names that have "Guide" as a substring should return 2 BookInfo objects:

    curl "localhost:8080/search?book-name=Guide"
    [{"name":"The Hitchhiker's Guide to the Galaxy","availability":"reserved","author":"Douglas Adams"},
    {"name":"Java Guide for Beginners","availability":"available"}]
  • The search for a substring "Gu" in name will return a "Bad Request" error, since we have defined the book-name parameter to have at least three characters:

    curl -i "localhost:8080/search?book-name=Gu"
    HTTP/1.1 400 Bad Request
    Content-Type: application/json
    date: ****
    content-length: 180
    connection: keep-alive
    
    {"message":"Bad Request","_embedded":{"errors":[{"message":"bookName: size must be between 3 and 2147483647"}]},
    "_links":{"self":{"href":"/search?book-name=Gu","templated":false}}}
  • Addition of a new book should not result in errors:

    curl -i -d '{"name": "My book", "availability": "available"}' \
      -H 'Content-Type: application/json' -X POST localhost:8080/add
    HTTP/1.1 200 OK
    date: Tue, 1 Feb 2022 00:01:57 GMT
    Content-Type: application/json
    content-length: 0
    connection: keep-alive

    You can then verify that the addition was successful by performing another search.

Testing the Application

To run the tests:

./gradlew test

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

Testing Models

As we have noticed previously, some files were generated as templates for tests. We will implement tests for models inside these files. Their main purpose will be to verify that we correctly described our API in the YAML file, and therefore the generated files behave as expected.

We will begin by writing tests for the required properties of BookInfo object. Define the following imports:

java/src/test/java/example/micronaut/model/BookInfoTest.java
import io.micronaut.context.annotation.Property;
import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Produces;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.rules.SecurityRule;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.annotation.security.PermitAll;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import jakarta.validation.Validator;

import static org.junit.jupiter.api.Assertions.*;

Add the following methods inside the BookInfoTest class:

java/src/test/java/example/micronaut/model/BookInfoTest.java

Validator will automatically validate parameters and response bodies annotated with @Valid in the controller. We will use it to test the validations manually. <2> Verify that the validator doesn’t produce any violations on a correct BookInfo instance. <3> Verify that null value is not allowed for the name property, since the property is marked as required. <4> Perform the same tests for the required availability property.

We will then write similar tests for other properties:

java/src/test/java/example/micronaut/model/BookInfoTest.java

Finally, we will test JSON serialization and parsing by writing a simple controller and client:

java/src/test/java/example/micronaut/model/BookInfoTest.java

this controller will only be used if the spec.name property is set to BookInfoTest. This will prevent the controller from running during other tests. <4> Define a GET method that will return a BookInfo object in the application/json format. <5> Create a test that will send a request to the server and verify that the response matches the desired object (This means that both serialization and parsing work correctly).

Similarly, we can implement tests for the BookAvailability class. The details are not shown in this guide.

Testing the Controller

We will write tests for the two paths of BookController.

Create a BooksControllerTest with the following contents:

java/src/test/java/example/micronaut/controller/BooksControllerTest.java

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.

Generate a Micronaut Application Native Executable with GraalVM

We will use GraalVM, an advanced JDK with ahead-of-time Native Image compilation, to generate a native executable of this Micronaut application.

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

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

GraalVM Installation

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

Java 25
sdk install java 25.0.2-graal

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

The previous command installs Oracle GraalVM, which is free to use in production and free to redistribute, at no cost, under the GraalVM Free Terms and Conditions.

Alternatively, you can use the GraalVM Community Edition:

Java 25
sdk install java 25.0.2-graalce

Native Executable Generation

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

Next Steps

Learn More

Read OpenAPI and Micronaut documentation and guides:

Add Security

We could have defined our security requirements by adding a security schema to the library-definition.yml file. For example, we will add HTTP Basic authentication:

Note
You can read more about describing various authentication in the "Authentication and Authorization" OpenAPI guide.

The generator will then annotate such endpoints with the Secured annotation accordingly:

@Secured(SecurityRule.IS_AUTHENTICATED)
public void addBook( /* ... */ ){ /* ... */ }

You will then need to implement an AuthenticationProvider that satisfies your needs. If you want to finish implementing the basic authentication, continue to the Micronaut Basic Auth guide and replicate steps to create the AuthenticationProvider and appropriate tests.

Note
You can also read Micronaut Security documentation or Micronaut guides about security to learn more about the supported Authorization strategies.

Generate Micronaut Client

You can generate a Micronaut client based on the same library-definition.yml file.

You can follow the "Use OpenAPI Definition to Generate a Micronaut Client" Guide for more information.

License

Note
All guides are released with an Apache License 2.0 for the code and a Creative Commons Attribution 4.0 license for the writing and media (images).