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();
}
}
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:
Spring Boot | Micronaut | |
---|---|---|
Mark a bean as a controller |
Annotate with |
Annotate with |
Identify a method as a GET endpoint |
Annotate with |
Annotate with |
Identify a method parameter as a path variable |
Annotate with Spring’s |
Annotate with Micronaut’s |
Respond HTTP Responses with a status code and a body |
Return a |
Return an |
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:
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:
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:
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.
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
.
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.
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…). |