3. POST - Spring Boot vs Micronaut Framework - Building a Rest API

This guide compares how to add POST endpoint in both Micronaut and Spring Boot applications.

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 write a POST endpoint backed by a persistence layer in a Micronaut Framework and Spring Boot applications.

The Spring Boot application uses Spring Data and H2. The Micronaut application uses Micronaut Data and H2. Micronaut Data is a database access toolkit that uses Ahead of Time (AoT) compilation to pre-compute queries for repository interfaces that are then executed by a thin, lightweight runtime layer.

This guide is the fourth 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

The controller receives a POST request with a JSON body, the repository persists the SaasSubscription, and then it returns a 201 response with a Location HTTP Header.

As you will see, both controllers are almost identical, with similar annotations, as we discussed in the second guide of this series.

The new APIs used in the following controllers are similar:

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

Identify a method as a POST endpoint

@PostMapping

@Post

Read and deserialize the request body to a method parameter

@RequestBody

@Body

Fluid ApI to build a URI

UriComponentsBuilder

UriBuilder

3.1. Spring Boot Controller

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

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

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

    private final SaasSubscriptionRepository repository;

    private SaasSubscriptionPostController(SaasSubscriptionRepository repository) { (3)
        this.repository = repository;
    }

    @PostMapping (4)
    private ResponseEntity<Void> createSaasSubscription(@RequestBody SaasSubscription newSaasSubscription, (5)
                                                        UriComponentsBuilder ucb) {
        SaasSubscription subscription = repository.save(newSaasSubscription);
        URI locationOfSubscription = ucb
                .path("subscriptions/{id}")
                .buildAndExpand(subscription.id())
                .toUri();
        return ResponseEntity.created(locationOfSubscription).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 Use constructor injection to inject a bean of type SaasSubscriptionRepository.
4 The @PostMapping annotation maps the createSaasSubscription method to an HTTP POST request on /subscriptions.
5 You can use the @RequestBody annotation to have the request body read and deserialized into an Object.

3.2. Micronaut Controller

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

import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.uri.UriBuilder;

import java.net.URI;

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

    private final SaasSubscriptionRepository repository;

    SaasSubscriptionPostController(SaasSubscriptionRepository repository) { (2)
        this.repository = repository;
    }

    @Post (3)
    HttpResponse<?> createSaasSubscription(@Body SaasSubscription newSaasSubscription) { (4)
        SaasSubscription savedSaasSubscription = repository.save(newSaasSubscription);
        URI locationOfNewSaasSubscription = UriBuilder.of("/subscriptions") (5)
                .path(savedSaasSubscription.id().toString())
                .build();
        return HttpResponse.created(locationOfNewSaasSubscription);
    }
}
1 The class is defined as a controller with the @Controller annotation mapped to the path /subscriptions.
2 Use constructor injection to inject a bean of type SaasSubscriptionRepository.
3 The @Post annotation maps the createSaasSubscription method to an HTTP POST request on /subscriptions.
4 The @Body annotation indicates that the request body should be parsed by Micronaut Framework and bound to the annotated parameter.
5 UriBuilder API allows you to create a java.net.URI easily.

4. Tests

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

The test saves a subscription and then uses the response Location header to fetch the saved subscription. It then compares if the GET response matches what we sent in the POST request.

4.1. Spring Boot Test

The following test shows that retrieving the status code or the location header from a ResponseEntity is easy.

springboot/src/test/java/example/micronaut/SaasSubscriptionPostControllerTest.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 java.net.URI;

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

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

    @Autowired (2)
    TestRestTemplate restTemplate;  (3)

    @Test
    void shouldCreateANewSaasSubscription() {
        SaasSubscription newSaasSubscription = new SaasSubscription(null, "Advanced", 2500);
        ResponseEntity<Void> createResponse = restTemplate.postForEntity("/subscriptions", newSaasSubscription, Void.class);
        assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);

        URI locationOfNewSaasSubscription = createResponse.getHeaders().getLocation();
        ResponseEntity<String> getResponse = restTemplate.getForEntity(locationOfNewSaasSubscription, String.class);
        assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);

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

        Integer cents = documentContext.read("$.cents");
        assertThat(cents).isEqualTo(2500);
    }
}
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 following test shows that retrieving the status code or the location header from a HttpResponse is easy.

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

import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import io.micronaut.http.HttpHeaders;
import io.micronaut.http.HttpRequest;
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.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;

import java.net.URI;
import java.util.Optional;

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

@MicronautTest (1)
class SaasSubscriptionPostControllerTest {

    @Test
    void shouldCreateANewSaasSubscription(@Client("/") HttpClient httpClient) { (2)
        BlockingHttpClient client = httpClient.toBlocking();
        SaasSubscription subscription = new SaasSubscription(100L, "Advanced", 2900);

        HttpResponse<Void> createResponse = client.exchange(HttpRequest.POST("/subscriptions", subscription), Void.class);
        assertThat(createResponse.getStatus().getCode()).isEqualTo(HttpStatus.CREATED.getCode());
        Optional<URI> locationOfNewSaasSubscriptionOptional = createResponse.getHeaders().get(HttpHeaders.LOCATION, URI.class);
        assertThat(locationOfNewSaasSubscriptionOptional).isPresent();

        URI locationOfNewSaasSubscription = locationOfNewSaasSubscriptionOptional.get();
        HttpResponse<String> getResponse = client.exchange(HttpRequest.GET(locationOfNewSaasSubscription), String.class);
        assertThat(getResponse.getStatus().getCode()).isEqualTo(HttpStatus.OK.getCode());

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

        Integer cents = documentContext.read("$.cents");
        assertThat(cents).isEqualTo(2900);
    }
}
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.

5. Conclusion

As you see in this guide, the code to write a POST endpoint backed by a persistence layer in Micronaut Framework and Spring Boot applications is very similar. However, the Micronaut compile-time approach does not require reflection or proxies, leading to faster and more efficient applications.

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