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

Authors: Andriy Dmytruk

Micronaut Version: 3.7.0

1. 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.

1.1. 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.

1.2. 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.

    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.

1.3. What The OpenAPI Generator Is

OpenAPI Generator allows the 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).

1.4. 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 OpenAPIGenerator 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 MySQL database from our application to store and retrieve data. You will complement the application with tests.

1.5. 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.

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. 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.

4. 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 .yaml 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.yaml and open it in your favourite text editor.

4.1. Describing General Server Info

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

src/main/resources/library-definition.yaml
openapi: 3.0.0 (1)
info: (2)
  description: This is a library API
  version: 1.0.0
  title: Library
  license:
    name: Apache-2.0
    url: "https://www.apache.org/licenses/LICENSE-2.0.html"
tags: (3)
  - name: books
    description: Search for books and add new ones
1 The version that will be used for parsing.
2 The info object contains general information about the API like a short description and license. In this case, we will be creating a website for a library.
3 Tags will be used to logically structure different paths.
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.

4.2. 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:

src/main/resources/library-definition.yaml
paths:

  /search:
    get: (1)
      tags: 
        - books (2)
      summary: Search for a book
      operationId: search (3)
      parameters: (4)
        - name: book-name
          in: query
          schema: 
            type: string
            minLength: 3 (5)
        - name: author-name
          in: query
          schema: 
            type: string
      responses: (6)
        "200": (7)
          description: Success
          content:
            "applicaton/json":
              schema:
                type: array, 
                items: 
                  $ref: "#/components/schemas/BookInfo"
        "400": (8)
          description: Bad Request
1 We define the GET operation on the /search path.
2 We use the books tag that we previously defined. Note that for each tag a controller will be generated that will implement its operations.
3 The search operation id will be used as method name for the given path.
4 We define two parameters of type string that the user should supply in the query.
5 Validation can be used on parameters. In this case, book name must contain at least three characters.
6 The responses object describes the response codes that can be produced. It also defines the structure of body if any.
7 In case of correct request, we define the body to contain a list of BookInfo objects. The schema for the book info object will be defined later in components/schemas section of the definition.
8 The "400" status code will be produced by Micronaut in case of a bad request, like an incorrect type supplied or failed validation. Even though Micronaut handles it automatically and no implementation is needed on our side, we add it for a complete API specification.
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 info about a book in our library. In this case, the request will contain a body with all the book information:

src/main/resources/library-definition.yaml
  /add:
    post: (1)
      tags: [books]
      summary: Add a new book
      operationId: addBook
      requestBody: (2)
        required: true
        content:
          "application/json": 
            schema: 
              $ref: "#/components/schemas/BookInfo" (3)
      responses:
        "200":
          description: Success
        "400":
          description: Bad Request
1 We define the POST method for the /add path, and add the same tag books to it.
2 We specify that a body is required and what are the supported content-types for it. (in this case only application/json, but multiple can be allowed).
3 We write that BookInfo object is required to be in the request body. We reference the same BookInfo schema that we will define next.
To read more about body definitions, see the "Describing Request Body" OpenAPI guide.

4.3. Defining Schemas

Schemas are required whenever a parameter, request body or a 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:

src/main/resources/library-definition.yaml
components:
  schemas:
    BookInfo:
      title: Book Info (1)
      description: Object containg all the info about a book
      type: object
      properties: (2)
        name: {type: string}
        availability: {$ref: "#/components/schemas/BookAvailability"} (3)
        author: {type: string, minLength: 3}
        ISBN: {type: string, pattern: "[0-9]{13}"}
      required: ["name", "availability"]
    BookAvailability: (4)
      type: string
      enum: ["available", "not available", "reserved"]
1 We define the BookInfo schema inside then components/schemas section. From this schema a Java class will be generated with the same BookInfo class name.
2 We define all the properties of BookInfo, including required validation on them (In this case, it is a minimal length requirement on one string and a regex pattern on another). An abbreviated form is used for some YAML lists and dictionaries to reduce the number of rows and simplify readability.
3 We reference another schema to be used as a property.
4 We define BookAvailability schema to be an enum with three available values. A Java BookAvailability class will be generated with given enum values based on our definition.

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.

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.

5. 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 the terminal in the same directory as library-definition.yaml file and run the following command:

java -jar openapi-generator-cli-XXX.jar generate \
    -g java-micronaut-server \(1)
    -i library-definition.yaml \(2)
    -o ./ \(3)
    -p controllerPackage=example.micronaut.controller \(4)
    -p modelPackage=example.micronaut.model \(5)
    -p build=gradle \(6)
    -p test=junit(7)
1 Specify that we will use Java Micronaut server generator.
2 Specify our OpenAPI definition file as library-definition.yaml, which we just created.
3 Specify the output directory to be the current directory (./). You can specify it to be a different one if you want (e.g. library-server).
4 We provide generator-specific properties starting with -p. We want all the controllers to be generated in the example.micronaut.controller package.
5 We want all the models (data models, like BookInfo) to be in example.micronaut.model package.
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.

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

java -jar openapi-generator-cli-XXX.jar config-help \
    -g java-micronaut-server

If you plan to change the definition file and regenerate files, consider setting the -p generateControllerAsAbstract=true parameter (we don’t recommend doing it during this guide, though). In this case, an abstract class will be generated for the API, while all the logic needs to be implemented in a different class (that extends the API abstract class). Your changes won’t be overwritten by generation, but the API will be updated.

After running, the OpenAPI generator CLI will output information about generated files. Now you can open the directory in your favorite IDE or text editor.

You should see the following directory structure:

./
├── docs
│   └── ... (1)
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── example/micronaut/
│   │   │       ├── Application.java (2)
│   │   │       ├── controller
│   │   │       │   └── BooksController.java (3)
│   │   │       └── model
│   │   │           ├── BookAvailability.java (4)
│   │   │           └── BookInfo.java
│   │   └── resources/
│   │       ├── application.yml (5)
│   │       └── logback.xml
│   └── test/
│       └── java/
│           └── example/micronaut/ (6)
│               ├── controller
│               │   └── BooksControllerTest.java
│               └── model
│                   ├── BookAvailabilityTest.java
│                   └── BookInfoTest.java
├── README.md
└── ...
1 The docs/ directory contains automatically generated Markdown files with documentation about your API.
2 Starts the Micronaut server with detected controllers.
3 The BooksController is generated based on paths with books tag. It is generated in the package we specified for controllers earlier.
4 Two files are generated in the models/ directory based on schemas we provided in the definition.
5 Config file for Micronaut is generated with a default value for server port and other parameters.
6 Tests are generated for all the controllers and models.

6. 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
1 The controller will receive client requests utilizing Micronaut server features.
2 The controller will call repository methods responsible for interaction with the database.
3 The repository methods will be implemented utilizing Micronaut JDBC, and will send queries to the database.
4 The files we generated with OpenAPI generator include Micronaut features responsible for server-client communication, like parameter and body binding, and JSON conversion.

7. Data Storage And Access With MySQL and JDBC

We will use MySQL 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.

7.1. Configure Access for a Data Source

We will use Micronaut Data JDBC to access the MySQL 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("mysql-connector-java:mysql")

Locally, the database will be provided by Test Resources.

src/main/resources/application.yml
datasources:
  default: (1)
    dialect: MYSQL
    driverClassName: ${JDBC_DRIVER:com.mysql.cj.jdbc.Driver} (2)
1 Create datasource called default.
2 Set the dialect and driver class name.

With the configured data source we will be able to access the data using Micronaut JDBC API, which will be shown further in the guide.

7.2. 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")
runtimeOnly("org.flywaydb:flyway-mysql")

We will enable Flyway in application.yml and configure it to perform migrations on one of the defined data sources.

src/main/resources/application.yml
flyway:
  datasources:
    default:
      enabled: true (1)
1 Enable Flyway for the default datasource.
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:

src/main/resources/db/migration/V1__schema.sql
CREATE TABLE book (
    id  BIGINT NOT NULL AUTO_INCREMENT UNIQUE PRIMARY KEY,
    name  VARCHAR(255) NOT NULL,
    availability  ENUM('available', 'reserved', 'not available') 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 sampe rows.

7.3. 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.

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

import example.micronaut.model.BookAvailability;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.data.annotation.MappedProperty;

import javax.validation.constraints.NotNull;

@MappedEntity("book") (1)
public class BookEntity {

    @Id (2)
    @GeneratedValue(GeneratedValue.Type.AUTO)
    private Long id;

    @NonNull
    @NotNull
    private String name;

    @NonNull
    @NotNull
    private BookAvailability availability;

    @Nullable
    private String author;

    @Nullable
    @MappedProperty("ISBN")
    private String isbn;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @NonNull
    public BookAvailability getAvailability() {
        return availability;
    }

    public void setAvailability(@NonNull BookAvailability availability) {
        this.availability = availability;
    }

    @Nullable
    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(@Nullable String isbn) {
        this.isbn = isbn;
    }

    @NonNull
    public String getName() {
        return name;
    }

    public void setName(@NonNull String name) {
        this.name = name;
    }

    @Nullable
    public String getAuthor() {
        return author;
    }

    public void setAuthor(@Nullable String author) {
        this.author = author;
    }
}
1 Annotate the class with @MappedEntity to map the class to the table defined in the schema.
2 Specifies the ID of an entity

7.4. 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.

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

import example.micronaut.model.BookAvailability;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.GenericRepository;
import io.micronaut.data.repository.jpa.JpaSpecificationExecutor;
import io.micronaut.data.repository.jpa.criteria.PredicateSpecification;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.List;

@JdbcRepository(dialect = Dialect.MYSQL) (1)
public interface BookRepository extends GenericRepository<BookEntity, Long>, (2)
        JpaSpecificationExecutor<BookEntity> { (3)

    @NonNull
    List<BookEntity> findAll(PredicateSpecification<BookEntity> spec);  (4)

    @NonNull
    List<BookEntity> findAll();


    @NonNull
    BookEntity save(@NonNull @NotBlank String name,
                    @NonNull @NotNull BookAvailability availability,
                    @NonNull @NotBlank String author,
                    @NonNull @NotBlank String isbn);
}
1 @JdbcRepository with a specific dialect.
2 BookEntity, the entity to treat as the root entity for the purposes of querying, is established either from the method signature or from the generic type parameter specified to the GenericRepository interface.
3 Implement the JpaSpecificationExecutor interface when you need create queries dynamically by composing JPA criteria.
4 To find multiple entities, you can use the findAll method from the JpaSpecificationExecutor interface.

In the above code snippet, we extended the JpaSpecificationExecutor interface to define a findAll method that supports Predicate as argument, which allows to modify the operations performed during runtime. We will now create a factory class for creating predicates that we plan to use in our application:

src/main/java/example/micronaut/BookSpecifications.java
package example.micronaut;

import io.micronaut.core.annotation.NonNull;
import io.micronaut.data.repository.jpa.criteria.PredicateSpecification;

public class BookSpecifications {
    public static PredicateSpecification<BookEntity> nameLike(@NonNull String name) {
        return (root, criteriaBuilder) -> criteriaBuilder.like(root.get("name"), "%"+name+"%");
    }

    public static PredicateSpecification<BookEntity> authorLike(@NonNull String author) {
        return (root, criteriaBuilder) -> criteriaBuilder.like(root.get("author"), "%"+author+"%");
    }
}

8. 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.

8.1. 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 should also have two methods named the same as the operations we created in the definition file. The methods have Micronaut framework annotations describing the required API. We will now write their bodies.

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:

src/main/java/example/micronaut/controller/BooksController.java
private final BookRepository bookRepository; (1)

public BooksController(BookRepository bookRepository) { (1)
    this.bookRepository = bookRepository;
}
1 Use constructor injection to inject a bean of type BookRepository.

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

src/main/java/example/micronaut/controller/BooksController.java
@ExecuteOn(TaskExecutors.IO) (1)
public List<BookInfo> search(
        @QueryValue(value="book-name") @Nullable @Size(min=3) String bookName,
        @QueryValue(value="author-name") @Nullable String authorName) {
    return searchEntities(bookName, authorName)
            .stream()
            .map(this::map) (5)
            .collect(Collectors.toList());
}

private BookInfo map(BookEntity entity) {
    BookInfo book = new BookInfo(entity.getName(), entity.getAvailability());
    book.setISBN(entity.getIsbn());
    book.setAuthor(entity.getAuthor());
    return book;
}

@NonNull
private List<BookEntity> searchEntities(@Nullable String name, @Nullable String author) { (2)
    if (StringUtils.isEmpty(name) && StringUtils.isEmpty(author)) {
        return bookRepository.findAll();
    } else if (StringUtils.isEmpty(name)) {
        return bookRepository.findAll(BookSpecifications.authorLike(author)); (3)

    } else  if (StringUtils.isEmpty(author)) {
        return bookRepository.findAll(BookSpecifications.nameLike(name));
    } else {
        return bookRepository.findAll(BookSpecifications.authorLike(author)
                .and(BookSpecifications.nameLike(name))); (4)
    }
}
1 It is critical that any blocking I/O operations (such as fetching the data from the database) are offloaded to a separate thread pool that does not block the Event loop.
2 Define the searchEntities method that will manage the different combinations of desired search parameters.
3 Use the predicate we previously defined to search for substring in one column
4 Use the Criteria API to build a query for combined search in the 2 columns during runtime.
5 Map the BookEntity instances to the desired return type.

Finally, we will implement the addBook method:

src/main/java/example/micronaut/controller/BooksController.java
@ExecuteOn(TaskExecutors.IO) (1)
@Status(OK) (2)
public void addBook(@Body @NotNull @Valid BookInfo bookInfo) {
    bookRepository.save(bookInfo.getName(), (3)
            bookInfo.getAvailability(),
            bookInfo.getAuthor(),
            bookInfo.getISBN());
}
1 It is critical that any blocking I/O operations (such as fetching the data from the database) are offloaded to a separate thread pool that does not block the Event loop.
2 You can return void in your controller’s method and specify the HTTP status code via the @Status annotation.
3 Call the repository method that will add an entry to the table.

9. Test Resources

When the application is started locally — either under test or by running the application — resolution of the datasource URL is detected and the Test Resources service will start a local MySQL 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.

10. 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 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.

11. Testing the Application

To run the tests:

./gradlew test

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

11.1. 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:

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.test.extensions.junit5.annotation.MicronautTest;
import jakarta.annotation.security.PermitAll;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import javax.validation.Validator;

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

Add the following methods inside the BookInfoTest class:

src/test/java/example/micronaut/model/BookInfoTest.java
    @Inject
    Validator validator; (1)

    @Test
    public void nameTest() {
        BookInfo bookInfo = new BookInfo("Alice's Adventures in Wonderland", BookAvailability.AVAILABLE);
        assertTrue(validator.validate(bookInfo).isEmpty()); (2)

        bookInfo = new BookInfo(null, BookAvailability.AVAILABLE);
        assertFalse(validator.validate(bookInfo).isEmpty()); (3)
    }

    @Test
    public void availabilityTest() { (4)
        BookInfo bookInfo = new BookInfo("ALice's Adventures in Wonderland", BookAvailability.RESERVED);
        assertTrue(validator.validate(bookInfo).isEmpty());

        bookInfo = new BookInfo("Alice's Adventures in Wonderland", null);
        assertFalse(validator.validate(bookInfo).isEmpty());
    }
1 Instruct Micronaut to inject an instance of the Validator. 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:

src/test/java/example/micronaut/model/BookInfoTest.java
    @Test
    public void authorTest() {
        BookInfo bookInfo = new BookInfo("Alice's Adventures in Wonderland", BookAvailability.AVAILABLE)
                .author(null);
        assertTrue(validator.validate(bookInfo).isEmpty());

        bookInfo = new BookInfo("Alice's Adventures in Wonderland", BookAvailability.AVAILABLE)
                .author("Lewis Carroll");
        assertTrue(validator.validate(bookInfo).isEmpty()); (1)

        bookInfo = new BookInfo("Alice's Adventures in Wonderland", BookAvailability.AVAILABLE)
                .author("fo");
        assertFalse(validator.validate(bookInfo).isEmpty()); (2)
    }

    @Test
    public void ISBNTest() {
        BookInfo bookInfo = new BookInfo("Alice's Adventures in Wonderland", BookAvailability.AVAILABLE)
                .ISBN(null);
        assertTrue(validator.validate(bookInfo).isEmpty());

        bookInfo = new BookInfo("Alice's Adventures in Wonderland", BookAvailability.AVAILABLE)
                .ISBN("9783161484100");
        assertTrue(validator.validate(bookInfo).isEmpty()); (3)

        bookInfo = new BookInfo("Alice's Adventures in Wonderland", BookAvailability.AVAILABLE)
                .ISBN("9783161 84100");
        assertFalse(validator.validate(bookInfo).isEmpty()); (4)
    }
1 Verify that there are no violations for both null or "Lewis Carol" used as a value for the author property.
2 Verify that there is a violation if the name is too short (at least tree characters are required).
3 Verify that there are no violations for valid values of the ISBN property.
4 Verify that there is a violation if the value doesn’t match the required pattern (A space is present).

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

src/test/java/example/micronaut/model/BookInfoTest.java
@Property(name = "spec.name", value = "BookInfoTest") (2)
@MicronautTest
public class BookInfoTest {

    @Inject
    @Client("/")
    HttpClient httpClient;

    @Test
    public void bookInfoJsonSerialization() {
        BookInfo requiredBookInfo = new BookInfo("Alice's Adventures in Wonderland", BookAvailability.AVAILABLE)
                .author("Lewis Carroll")
                .ISBN("9783161484100");

        BookInfo bookInfo = httpClient.toBlocking().retrieve(HttpRequest.GET("/bookinfo"), BookInfo.class); (5)
        assertEquals(requiredBookInfo, bookInfo);
    }

    @Requires(property = "spec.name", value = "BookInfoTest") (3)
    @Controller("/bookinfo") (1)
    static class BookInfoSerdeController {
        @PermitAll
        @Get
        BookInfo index() { (4)
            return new BookInfo("Alice's Adventures in Wonderland", BookAvailability.AVAILABLE)
                    .author("Lewis Carroll")
                    .ISBN("9783161484100");
        }
    }
1 Create a simple controller that will respond to requests on the /bookinfo path.
2 Specify the spec.name property for this test class.
3 Use the Requires annotation to specify that 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.

11.2. Testing the Controller

We will write tests for the two paths of BookController.

If you open the BooksControllerTest, you can see that templates of tests were generated for both paths with examples of requests to corresponding paths. The templates can be used to simplify and speed up test writing.

We will simply replace the contents of the file:

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

import example.micronaut.model.BookAvailability;
import example.micronaut.model.BookInfo;
import io.micronaut.http.uri.UriBuilder;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MutableHttpRequest;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.core.type.Argument;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import jakarta.inject.Inject;
import java.util.List;

@MicronautTest (1)
public class BooksControllerTest {

    @Inject
    @Client("${context-path}")
    HttpClient client; (2)

    @Test
    void addBookClientApiTest() {
        BookInfo body = new BookInfo("Building Microservices", BookAvailability.AVAILABLE);
        body.setAuthor("Sam Newman");
        body.setISBN("9781492034025");
        HttpResponse<?> response = client.toBlocking()
                .exchange(HttpRequest.POST("/add", body)); (3)
        assertEquals(HttpStatus.OK, response.status()); (4)
    }

    @Test
    void searchClientApiTest() {
        HttpResponse<List<BookInfo>> response = client.toBlocking()
                .exchange(HttpRequest.GET(UriBuilder.of("/search")
                        .queryParam("book-name", "Guide")
                        .build()
                ), Argument.listOf(BookInfo.class)); (5)
        List<BookInfo> body = response.body(); (6)
        assertEquals(HttpStatus.OK, response.status());
        assertEquals(2, body.size()); (7)
    }
}
1 Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context and the embedded server. More info.
2 Inject the HttpClient bean and point it to the embedded server.
3 Creating HTTP Requests is easy thanks to the Micronaut framework fluid API.
4 Verify that addition of book info was successful by checking the status code.
5 Micronaut HTTP Client simplifies binding a JSON array to a list of POJOs by using Argument::listOf.
6 Use .body() to retrieve the parsed payload.
7 Verify that there are exactly two books with "Guide" substring in title.

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.

12. 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.

12.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.1.0.r11-grl
If you still use Java 8, use the JDK11 version of GraalVM.
Java 17
sdk install java 22.1.0.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

13. Next steps

13.1. Learn More

Read OpenAPI and Micronaut documentation and guides:

13.2. Add Security

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

paths:
  /search:
    # ... #
  /add:
    post:
      # ... #
      security:
        - MyBasicAuth: [] (2)
components:
  schemas:
    # ... #
  securitySchemes:
    MyBasicAuth: (1)
      type: http
      scheme: basic
1 Define a security schema inside the components/securitySchemes. We want to use Basic auth for authentication.
2 Add the schema to the paths that you want to secure. In this case, we want to restrict access to adding books into our library.
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 Mono<Object> 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.

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

13.3. Generate Micronaut Client

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

Run the following in terminal to create client in the library-client directory:

java -jar openapi-generator-cli-XXX.jar generate \
    -g java-micronaut-client \
    -i library-definition.yaml \
    -o library-client \
    -p apiPackage=example.micronaut.api \
    -p modelPackage=example.micronaut.model \
    -p build=gradle \
    -p test=junit

13.4. Add Server URL Information

If you have your server running, you can add your website URL to it in the YAML definition file:

# ... #
servers:
  - url: 'http://my.website.com'

13.5. Generate User-Friendly Documentation

You can generate documentation in html file inside the html-docs/ directory by running

java -jar openapi-generator-cli-XXX.jar generate \
    -g html2 \
    -i library-definition.yaml \
    -o html-docs