2. Implementing GET - Spring Boot vs Micronaut Framework - Building a Rest API

This guide compares how to implement a GET endpoint with Micronaut Framework and Spring Boot.

Authors: Sergio del Amo

Micronaut Version: 4.6.3

1. Sample Project

You can download a sample application with the code examples in this article.

2. Introduction

This guide compares how to test implement a GET in a Micronaut Framework and Spring Boot applications.

This guide is the second tutorial of Building a Rest API - a series of tutorials comparing how to develop a REST API with Micronaut Framework and Spring Boot.

3. Controller

To implement a GET /subscriptions/{id} endpoint, you need to create a controller in both frameworks.

The API is quite similar:

Table 1. Comparison between Spring Boot and Micronaut Framework
Spring Boot Micronaut

Mark a bean as a controller

Annotate with @RestController and @RequestMapping

Annotate with @Controller

Identify a method as a GET endpoint

Annotate with @GetMapping

Annotate with @Get

Identify a method parameter as a path variable

Annotate with Spring’s @PathVariable

Annotate with Micronaut’s @PathVariable

Respond HTTP Responses with a status code and a body

Return a ResponseEntity

Return an HttpResponse

Another important difference is the controller’s method visibility. Micronaut Framework does not use reflection (which leads to better performance and better integration with technologies such as GraalVM). Thus, it requires the controller’s methods to be public, protected, or package-private (no modifier). Throughout these tutorials, Micronaut controllers' methods use package-private.

3.1. Spring Boot Controller

This is the Spring Boot controller:

springboot/src/main/java/example/micronaut/SaasSubscriptionController.java
package example.micronaut;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController (1)
@RequestMapping("/subscriptions") (2)
class SaasSubscriptionController {

    @GetMapping("/{id}") (3)
    private ResponseEntity<SaasSubscription> findById(@PathVariable Long id) { (4)
        if (id.equals(99L)) {
            SaasSubscription subscription = new SaasSubscription(99L, "Advanced", 2900);
            return ResponseEntity.ok(subscription);
        }
        return ResponseEntity.notFound().build();
    }
}
1 Annotate a class @RestController to identify the class as a Component capable of handling HTTP requests.
2 @RequestMapping identifies the request paths that invoke this Controller.
3 The @GetMapping annotation maps the findById method to an HTTP GET request on /subscriptions/{id}.
4 Add the @PathVariable annotation to the handler method argument to make controller aware.

3.2. Micronaut Controller

This is the Micronaut controller:

micronautframework/src/main/java/example/micronaut/SaasSubscriptionController.java
package example.micronaut;

import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.PathVariable;

@Controller("/subscriptions") (1)
class SaasSubscriptionController {

    @Get("/{id}") (2)
    HttpResponse<SaasSubscription> findById(@PathVariable Long id) { (3)
        if (id.equals(99L)) {
            SaasSubscription subscription = new SaasSubscription(99L, "Advanced", 2900);
            return HttpResponse.ok(subscription);
        }
        return HttpResponse.notFound(); (4)
    }
}
1 The class is defined as a controller with the @Controller annotation mapped to the path /subscriptions.
2 The @Get annotation maps the findById method to an HTTP GET request on /subscriptions/{id}.
3 You can define path variables with a RFC-6570 URI template in the HTTP Method annotation value. The method argument can optionally be annotated with @PathVariable.
4 You can use the HTTPResponse fluid API to control the response’s status code, its body and headers.

3.2.1. Without HTTPResponse

The default HTTP Status code in a Micronaut controller method is 200. However, when a Micronaut controller’s method returns null, the application responds with a 404 status code.

Thus, you could simplify the previous controller as:

micronautframeworkjacksondatabind/src/main/java/example/micronaut/SaasSubscriptionController.java
package example.micronaut;

import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.PathVariable;

@Controller("/subscriptions") (1)
class SaasSubscriptionController {

    @Get("/{id}") (2)
    SaasSubscription findById(@PathVariable Long id) { (3)
        if (id.equals(99L)) {
            return new SaasSubscription(99L, "Advanced", 2900);
        }
        return null; (4)
    }
}
1 The class is defined as a controller with the @Controller annotation mapped to the path /subscriptions.
2 The @Get annotation maps the findById method to an HTTP GET request on /.
3 You can define path variables with a RFC-6570 URI template in the HTTP Method annotation value. The method argument can optionally be annotated with @PathVariable.
4 Returning null triggers a 404 (Not Found) response.

3.3. Route Compile-Time Validation

The Micronaut framework supports validating route arguments at compile time. Route arguments will automatically be checked at compile time if your application contains the following dependency.

build.gradle
annotationProcessor("io.micronaut:micronaut-http-validation")

This enables an early feedback loop. The earlier you catch errors, the less time you spend debugging, the less money it costs you to develop an application.

For example, if you replace the @Get("/{id}") annotation with @Get("/{identifier}"), the application fails to compile.

4. Tests

In this tutorial, we use AssertJ in the tests. Moreover, we use Jayway JsonPath - a Java DSL for reading JSON documents.

4.1. Spring Boot Test

We could write a test in Spring Boot using TestRestTemplate.

springboot/src/test/java/example/micronaut/SaasSubscriptionControllerGetTest.java
package example.micronaut;

import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) (1)
class SaasSubscriptionControllerGetTest {

    @Autowired (2)
    TestRestTemplate restTemplate; (3)

    @Test
    void shouldReturnASaasSubscriptionWhenDataIsSaved() {
        ResponseEntity<String> response = restTemplate.getForEntity("/subscriptions/99", String.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);

        DocumentContext documentContext = JsonPath.parse(response.getBody());
        Number id = documentContext.read("$.id");
        assertThat(id).isNotNull();
        assertThat(id).isEqualTo(99);

        String name = documentContext.read("$.name");
        assertThat(name).isNotNull();
        assertThat(name).isEqualTo("Advanced");

        Integer cents = documentContext.read("$.cents");
        assertThat(cents).isEqualTo(2900);
    }

    @Test
    void shouldNotReturnASaasSubscriptionWithAnUnknownId() {
        ResponseEntity<String> response = restTemplate.getForEntity("/subscriptions/1000", String.class);
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
        assertThat(response.getBody()).isBlank();
    }
}
1 The @SpringBootTest annotation tells Spring Boot to look for a main configuration class (one with @SpringBootApplication, for instance) and use that to start a Spring application context.
2 Inject a bean of type TestRestTemplate by using @Autowired on the field definition.
3 TestRestTemplate is a helper to ease to execution of HTTP requests against the locally running application.

4.2. Micronaut Test

The main difference with the Spring Boot Test, is that we use the Micronaut HTTP Client to test the embedded server.

micronautframework/src/test/java/example/micronaut/SaasSubscriptionControllerGetTest.java
package example.micronaut;

import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.BlockingHttpClient;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.catchThrowableOfType;

@MicronautTest (1)
class SaasSubscriptionControllerGetTest {

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

    @Test
    void shouldReturnASaasSubscriptionWhenDataIsSaved() {
        BlockingHttpClient client = httpClient.toBlocking();
        HttpResponse<String> response = client.exchange("/subscriptions/99", String.class);
        assertThat(response.status().getCode()).isEqualTo(HttpStatus.OK.getCode());

        DocumentContext documentContext = JsonPath.parse(response.body());
        Number id = documentContext.read("$.id");
        assertThat(id).isNotNull();
        assertThat(id).isEqualTo(99);

        String name = documentContext.read("$.name");
        assertThat(name).isNotNull();
        assertThat(name).isEqualTo("Advanced");

        Integer cents = documentContext.read("$.cents");
        assertThat(cents).isEqualTo(2900);
    }

    @Test
    void shouldNotReturnASaasSubscriptionWithAnUnknownId() {
        BlockingHttpClient client = httpClient.toBlocking();
        HttpClientResponseException thrown = catchThrowableOfType(() -> (3)
                client.exchange("/subscriptions/1000", String.class), HttpClientResponseException.class);
        assertThat(thrown.getStatus().getCode()).isEqualTo(HttpStatus.NOT_FOUND.getCode());
        assertThat(thrown.getResponse().getBody()).isEmpty();
    }
}
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 When the HTTP Client receives a response with an HTTP Status Code >= 400, it throws an HttpClientResponseException. You can obtain the response status and body from the exception.

5. Conclusion

Defining routes is extremely similar in both frameworks. However, Micronaut Framework provides compile-time validation of routes and a reflection-free approach to do it.

6. 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…​).