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

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=maven \
    --lang=kotlin \
    --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:

pom.xml
<dependency>
    <groupId>io.micronaut.flyway</groupId>
    <artifactId>micronaut-flyway</artifactId>
    <scope>compile</scope>
</dependency>

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/kotlin/example/micronaut/PetType.kt
package example.micronaut

enum class PetType {
    DOG,
    CAT
}

Create an entity:

src/main/kotlin/example/micronaut/Pet.kt
package example.micronaut

import io.micronaut.data.annotation.MappedEntity
import io.micronaut.serde.annotation.Serdeable
import io.micronaut.data.annotation.GeneratedValue
import io.micronaut.data.annotation.Id
import jakarta.validation.constraints.NotBlank

@Serdeable (1)
@MappedEntity (2)
data class Pet(@NotBlank val name: String,
               val type: PetType = PetType.DOG) {
    @Id (3)
    @GeneratedValue(GeneratedValue.Type.AUTO) var id: Long? = null (4)
}
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/kotlin/example/micronaut/NameDto.kt
package example.micronaut

import io.micronaut.serde.annotation.Serdeable

@Serdeable (1)
data class NameDto(val name: String)
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/kotlin/example/micronaut/PetRepository.kt
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.*
import jakarta.validation.constraints.NotBlank

@JdbcRepository(dialect = Dialect.MYSQL) (1)
interface PetRepository : CrudRepository<Pet, Long> { (2)
    fun list() : List<NameDto> (3)

    fun findByName(@NotBlank name: String) : Optional<Pet>

    fun save(@NotBlank name: String, type: PetType) : Pet
}
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:

pom.xml
<!-- Add the following to your annotationProcessorPaths element -->
<annotationProcessorPath>
    <groupId>io.micronaut.jaxrs</groupId>
    <artifactId>micronaut-jaxrs-processor</artifactId>
</annotationProcessorPath>
<dependency>
    <groupId>io.micronaut.jaxrs</groupId>
    <artifactId>micronaut-jaxrs-server</artifactId>
    <scope>compile</scope>
</dependency>

4.5.2. Resource

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

src/main/kotlin/example/micronaut/PetSave.kt
package example.micronaut

import io.micronaut.serde.annotation.Serdeable
import jakarta.validation.constraints.NotBlank

@Serdeable
data class PetSave(@NotBlank val name: String, val type: PetType)

Define an endpoint using JAX-RS.

src/main/kotlin/example/micronaut/PetResource.kt
package example.micronaut

import io.micronaut.http.HttpStatus
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Status
import jakarta.ws.rs.GET
import jakarta.ws.rs.POST
import jakarta.ws.rs.Path
import jakarta.ws.rs.PathParam

@Path("/pets") (1)
class PetResource(val petRepository: PetRepository) { (2)

    @GET (3)
    fun all() = petRepository.findAll() (4)

    @GET (3)
    @Path("/{name}") (4)
    fun byName(@PathParam("name") petsName: String) = petRepository.findByName(petsName) (5)

    @POST
    @Status(HttpStatus.CREATED)
    fun save(@Body petSave: PetSave) {
        petRepository.save(petSave.name, petSave.type)
    }
}
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/kotlin/example/micronaut/PetResourceTest.kt
package example.micronaut

import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
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 jakarta.inject.Inject
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test

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

    @Inject
    @field:Client("/")
    lateinit var httpClient : HttpClient (2)

    @Inject
    lateinit var repository: PetRepository

    @Test
    fun testAll() {
        val dino = Pet("Dino")
        val pb = Pet("Baby Puss", PetType.CAT)
        val hoppy = Pet("Hoppy")

        repository.saveAll(listOf(dino, pb, hoppy))

        val client = httpClient.toBlocking()
        val request: HttpRequest<Any> = HttpRequest.GET("/pets")
        val response = client.exchange<Any, MutableList<NameDto>>(request,
            Argument.listOf(NameDto::class.java))
        assertEquals(HttpStatus.OK, response.status)
        val petNames = response.body()
        assertNotNull(petNames)
        assertEquals(3, petNames.size)
        repository.deleteAll()
    }
    
    @Test
    fun testGet() {
        val name = "Dino"
        val dino = Pet(name)
        val id = repository.save(dino).id
        val uri = UriBuilder.of("/pets").path(name).build()
        val request : HttpRequest<Any> = HttpRequest.GET(uri)
        val client = httpClient.toBlocking()
        val response = client.exchange(request, Pet::class.java)
        assertEquals(HttpStatus.OK, response.status)
        val pet = response.body()
        assertNotNull(pet)
        if (pet != null) {
            assertNotNull(pet.id)
            assertNotNull(pet.type)
            assertEquals(PetType.DOG, pet.type)
            assertEquals(name, pet.name)
        }
        if (id != null) {
            repository.deleteById(id)
        }
    }

    @Test
    fun testGetIfPetDoesNotExistsAPIReturnsNotFound() {
        val name = "Dino"
        val dino = Pet(name)
        val id = repository.save(dino).id
        val uri = UriBuilder.of("/pets").path("Foo").build()
        val client = httpClient.toBlocking()
        val request : HttpRequest<Any> = HttpRequest.GET(uri)
        val thrown: HttpClientResponseException = assertThrows(HttpClientResponseException::class.java) {
            client.exchange(request, Pet::class.java)
        }
        assertEquals(HttpStatus.NOT_FOUND, thrown.response.status()) (3)
        if (id != null) {
            repository.deleteById(id)
        }
    }

    @Test
    fun testSave() {
        val name = "Dino"
        val oldCount = repository.count()
        val petSave = PetSave(name, PetType.DOG)
        val request: HttpRequest<Any> = HttpRequest.POST("/pets", petSave)
        val client = httpClient.toBlocking()
        val response = client.exchange(request, Pet::class.java)
        assertEquals(HttpStatus.CREATED, response.status())
        val 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:

./mvnw test

7. Running the Application

To run the application, use the ./mvnw mn: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 Maven, run:

./mvnw package -Dpackaging=native-image

The native executable is created in the target directory and can be run with target/micronautguide.

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-maven-kotlin.adoc - include::build/code/micronaut-jaxrs-jdbc/micronaut-jaxrs-jdbc-maven-kotlin/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…​).