Micronaut JAX-RS

Expose endpoints using JAX-RS annotations in a Micronaut application

Authors: Dan Hollingsworth, Sergio del Amo

Micronaut Version: 4.6.3

1. Getting Started

In this guide, we will create a Micronaut application written in Java.

By default, Micronaut users define their HTTP Routing using the Micronaut @Controller annotation and other built-in Routing Annotations. However, Micronaut JAX-RS allows you to define your Micronaut endpoints with JAX-RS annotations.

What exactly is JAX-RS? JAX-RS is a POJO-based, annotation-driven framework for building web services that comply with RESTful principles. Imagine writing all the low level code to parse an HTTP request and the logic just to wire these requests to appropriate Java classes/methods. The beauty of the JAX-RS API is that it insulates developers from that complexity and allows them to concentrate on business logic. That’s precisely where the use of POJOs and annotations come into play! JAX-RS has annotations to bind specific URI patterns and HTTP operations to individual methods of your Java class.

This application exposes some REST endpoints using JAX-RS annotations and stores data in a MySQL database using Micronaut Data JDBC.

2. What you will need

To complete this guide, you will need the following:

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. Writing the Application

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

mn create-app example.micronaut.micronautguide \
    --features=jax-rs,data-jdbc,mysql,flyway,serialization-jackson,validation,graalvm \
    --build=gradle \
    --lang=java \
    --test=junit
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 jax-rs, data-jdbc, mysql, flyway, serialization-jackson, validation, and graalvm features.

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.

4.1. Data Source configuration

Define the datasource in src/main/resources/application.properties.

src/main/resources/application.properties
datasources.default.db-type=mysql
datasources.default.dialect=MYSQL
datasources.default.driver-class-name=com.mysql.cj.jdbc.Driver
This way of defining the datasource properties enables us to externalize the configuration, for example for production environment, and also provide a default value for development. If the environment variables are not defined, the Micronaut framework will use the default values.

4.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")

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

src/main/resources/application.properties
(1)
flyway.datasources.default.enabled=true
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
DROP TABLE IF EXISTS pet;

CREATE TABLE pet (
    id   BIGINT NOT NULL AUTO_INCREMENT UNIQUE PRIMARY KEY,
    name  VARCHAR(255) NOT NULL UNIQUE,
    type varchar(255) check (type in ('DOG', 'CAT'))
);

During application startup, Flyway will execute the SQL file and create the schema needed for the application.

4.3. Domain

Add an enum:

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

public enum PetType {
    DOG,
    CAT
}

Create an entity:

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

import io.micronaut.core.annotation.NonNull;
import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.serde.annotation.Serdeable;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

@Serdeable (1)
@MappedEntity (2)
public class Pet {

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

    @NonNull
    @NotBlank
    private String name;

    @NonNull
    @NotNull
    private PetType type = PetType.DOG;

    public Pet() {
    }

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

    public Pet(@NonNull String name, @NonNull PetType type) {
        this.name = name;
        this.type = type;
    }

    public Long getId() {
        return id;
    }

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

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

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

    @NonNull
    public PetType getType() {
        return type;
    }

    public void setType(@NonNull PetType type) {
        this.type = type;
    }
}
1 Declare the @Serdeable annotation at the type level in your source code to allow the type to be serialized or deserialized.
2 Annotate the class with @MappedEntity to map the class to the table defined in the schema.
3 Specifies the ID of an entity
4 Specifies that the property value is generated by the database and not included in inserts

4.4. Service

Create a POJO NameDto:

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

import io.micronaut.core.annotation.NonNull;
import io.micronaut.serde.annotation.Serdeable;

import jakarta.validation.constraints.NotBlank;

@Serdeable (1)
public class NameDto {

    @NonNull
    @NotBlank
    private final String name;

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

    @NonNull
    public String getName() {
        return name;
    }
}
1 Declare the @Serdeable annotation at the type level in your source code to allow the type to be serialized or deserialized.

Create a Repository:

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

import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;
import java.util.List;
import java.util.Optional;
import io.micronaut.core.annotation.NonNull;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

@JdbcRepository(dialect = Dialect.MYSQL) (1)
public interface PetRepository extends CrudRepository<Pet, Long> { (2)

    @NonNull
    List<NameDto> list(); (3)

    @NonNull
    Optional<Pet> findByName(@NonNull @NotBlank String name);

    Pet save(@NonNull @NotBlank String name,
             @NonNull @NotNull PetType type);
}
1 @JdbcRepository with a specific dialect.
2 By extending CrudRepository you enable automatic generation of CRUD (Create, Read, Update, Delete) operations.
3 Micronaut Data supports reflection-free Data Transfer Object (DTO) projections if the return type is annotated with @Introspected.

4.5. JAX-RS

4.5.1. Dependencies

When you add a jax-rs feature, the generated application includes the following dependencies:

build.gradle
annotationProcessor("io.micronaut.jaxrs:micronaut-jaxrs-processor")
implementation("io.micronaut.jaxrs:micronaut-jaxrs-server")

4.5.2. Resource

Create a POJO to encapsulate the HTTP Request body for a save request:

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

import io.micronaut.core.annotation.NonNull;
import io.micronaut.serde.annotation.Serdeable;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

@Serdeable
public class PetSave {
    @NonNull
    @NotBlank
    private final String name;

    @NonNull
    @NotNull
    private final PetType type;

    public PetSave(@NonNull String name, @NonNull PetType type) {
        this.name = name;
        this.type = type;
    }

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

    @NonNull
    public PetType getType() {
        return type;
    }
}

Define an endpoint using JAX-RS.

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

import io.micronaut.core.annotation.NonNull;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Status;

import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import java.util.List;
import java.util.Optional;

@Path("/pets") (1)
class PetResource {

    private final PetRepository petRepository;

    PetResource(PetRepository petRepository) { (2)
        this.petRepository = petRepository;
    }

    @GET (3)
    public List<NameDto> all() { (4)
        return petRepository.list();
    }

    @GET (3)
    @Path("/{name}") (5)
    public Optional<Pet> byName(@PathParam("name") String petsName) { (6)
        return petRepository.findByName(petsName);
    }

    @POST
    @Status(HttpStatus.CREATED)
    public void save(@NonNull @NotNull @Body PetSave petSave) {
        petRepository.save(petSave.getName(), petSave.getType());

    }
}
1 A JAX-RS annotation to define the base URI for methods in this class.
2 Use constructor injection to inject a bean of type PetRepository.
3 A JAX-RS annotation to define an endpoint for HTTP Get requests. Mapped to the base URI /pets, since no @Path is provided.
4 The response is converted to a JSON array automatically.
5 Paths can be templated for path parameters, such as the pet’s name, here.
6 Associate the template placeholder name with the Java variable.

4.6. Tests

Add a test for the resource:

src/test/java/example/micronaut/PetResourceTest.java
package example.micronaut;

import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.http.uri.UriBuilder;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import jakarta.inject.Inject;
import org.junit.jupiter.api.function.Executable;

import java.net.URI;
import java.util.Arrays;
import java.util.List;

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

@MicronautTest(transactional = false) (1)
public class PetResourceTest {

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

    @Inject
    PetRepository repository;

    @Test
    void testAll() {
        Pet dino = new Pet("Dino");
        Pet bp = new Pet("Baby Puss");
        bp.setType(PetType.CAT);
        Pet hoppy = new Pet("Hoppy");

        repository.saveAll(Arrays.asList(dino, bp, hoppy));

        HttpResponse<List<NameDto>> response = httpClient.toBlocking()
                .exchange(HttpRequest.GET("/pets"), Argument.listOf(NameDto.class));
        assertEquals(HttpStatus.OK , response.status());
        List<NameDto> petNames = response.body();
        assertNotNull(petNames);
        assertEquals(3, petNames.size());
        repository.deleteAll();
    }

    @Test
    void testGet() {
        String name = "Dino";
        Pet dino = new Pet(name);
        Long id = repository.save(dino).getId();

        URI uri = UriBuilder.of("/pets").path(name).build();
        HttpResponse<Pet> response = httpClient.toBlocking()
                .exchange(HttpRequest.GET(uri), Pet.class);
        assertEquals(HttpStatus.OK , response.status());
        Pet pet = response.body();
        assertNotNull(pet);
        assertNotNull(pet.getId());
        assertNotNull(pet.getType());
        assertEquals(PetType.DOG, pet.getType());
        assertEquals(name, pet.getName());
        repository.deleteById(id);
    }

    @Test
    void testGetIfPetDoesNotExistsAPIReturnsNotFound() {
        String name = "Dino";
        Pet dino = new Pet(name);
        Long id = repository.save(dino).getId();

        URI uri = UriBuilder.of("/pets").path("Foo").build();
        Executable e = () -> httpClient.toBlocking()
                .exchange(HttpRequest.GET(uri), Pet.class);
        HttpClientResponseException thrown = assertThrows(HttpClientResponseException.class, e);
        assertEquals(HttpStatus.NOT_FOUND , thrown.getResponse().status()); (2)

        repository.deleteById(id);
    }

    @Test
    void testSave() {
        String name = "Dino";
        long oldCount = repository.count();
        PetSave petSave = new PetSave(name, PetType.DOG);
        HttpRequest<?> request = HttpRequest.POST("/pets", petSave);
        HttpResponse<Pet> response = httpClient.toBlocking()
                .exchange(request, Pet.class);
        assertEquals(HttpStatus.CREATED , response.status());
        long count = repository.count();
        assertEquals(oldCount + 1, count);
        repository.deleteAll();
    }
}
1 Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context and the embedded server. By default, each @Test method will be wrapped in a transaction that will be rolled back when the test finishes. This behaviour is is changed by setting transaction to false.
2 Inject the HttpClient bean and point it to the embedded server.
3 Micronaut returns a 404 response if the resource method returns an empty optional.

5. 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 or R2DBC section of the Test Resources documentation.

6. Testing the Application

To run the tests:

./gradlew test

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

7. Running the Application

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

You can execute the endpoints exposed by the application:

curl -id '{"name":"Chase", "type":"DOG"}' \
     -H "Content-Type: application/json" \
     -X POST http://localhost:8080/pets
HTTP/1.1 201 Created
...
curl -i localhost:8080/pets
HTTP/1.1 200 OK
...
[{"name":"Chase"}]

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

8.1. GraalVM installation

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

Java 21
sdk install java 21.0.5-graal

For installation on Windows, or for 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 21
sdk install java 21.0.2-graalce

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

You can execute the endpoints exposed by the native executable:

curl -id '{"name":"Chase", "type":"DOG"}' \
     -H "Content-Type: application/json" \
     -X POST http://localhost:8080/pets
HTTP/1.1 201 Created
...
curl -i localhost:8080/pets
HTTP/1.1 200 OK
...
[{"name":"Chase"}]

9. Configuration for production

When the application is run in a non-local environment, you will need to specify the datasource URL and credentials in a configuration that matches the specific environment. This can be achieved by adding a configuration specific to that environment like so:

src/main/resources/application-prod.yml
Unresolved directive in micronaut-jaxrs-jdbc-gradle-java.adoc - include::build/code/micronaut-jaxrs-jdbc/micronaut-jaxrs-jdbc-gradle-java/src/main/resources/application-prod.yml[tag=prod-datasource]

And then run the application with the following environment variables:

  • MICRONAUT_ENVIRONMENTS=prod

  • DATASOURCES_DEFAULT_USERNAME=<username>

  • DATASOURCES_DEFAULT_PASSWORD=<password>

Instead of environment variables, you can also use Micronaut Distributed Configuration to pull these values from a secrets manager such as Hashicorp Vault.

10. Next steps

11. License

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