Notice: Function _load_textdomain_just_in_time was called incorrectly. Translation loading for the ga-google-analytics domain was triggered too early. This is usually an indicator for some code in the plugin or theme running too early. Translations should be loaded at the init action or later. Please see Debugging in WordPress for more information. (This message was added in version 6.7.0.) in /home/posttrau/public_html/mdtWordpress/wp-includes/functions.php on line 6121

Notice: Function _load_textdomain_just_in_time was called incorrectly. Translation loading for the themeisle-companion domain was triggered too early. This is usually an indicator for some code in the plugin or theme running too early. Translations should be loaded at the init action or later. Please see Debugging in WordPress for more information. (This message was added in version 6.7.0.) in /home/posttrau/public_html/mdtWordpress/wp-includes/functions.php on line 6121

Notice: Function _load_textdomain_just_in_time was called incorrectly. Translation loading for the foxiz-core domain was triggered too early. This is usually an indicator for some code in the plugin or theme running too early. Translations should be loaded at the init action or later. Please see Debugging in WordPress for more information. (This message was added in version 6.7.0.) in /home/posttrau/public_html/mdtWordpress/wp-includes/functions.php on line 6121

Notice: Function _load_textdomain_just_in_time was called incorrectly. Translation loading for the hestia domain was triggered too early. This is usually an indicator for some code in the plugin or theme running too early. Translations should be loaded at the init action or later. Please see Debugging in WordPress for more information. (This message was added in version 6.7.0.) in /home/posttrau/public_html/mdtWordpress/wp-includes/functions.php on line 6121
How to build a Spring Reactive API - My Day To-Do

If you are a Java programmer chances are that you have worked with Spring Boot and you may or may not have heard of project reactor, WebFlux etc. In case you haven’t, then Spring Project Reactor is a spring library with which you can build Spring Reactive systems i.e. asynchronous or non-blocking APIs. This post will avoid getting into too many details for reactive systems as that topic deserves it’s own dedicated blogpost. I will write that sometime in the future. Hence, this post will focus on integration with Spring via frameworks and other implementation details. In this post you will build a reactive Spring API using WebFlux that saves data in MongoDB and manages it using Spring data-mongodb-reactive.

Reactive systems are here to stay, and it would only make sense for you to get familiar with it.

Build spring reactive api

For this let’s follow a logical approach whereby you will first be coding the controllers, followed by the classes in the Service layer followed by the repository. In addition to all this, you will also be adding some unit tests for both the controller and the service layer.

A simple cars API with a MongoDB backend

As you may have noticed from the above paragraph, in this tutorial you will build a simple Spring reactive API to CRUD car data to a MongoDB database.

CarController

import com.mydaytodo.tutorials.spring.reactive.mongo.model.Car;
import com.mydaytodo.tutorials.spring.reactive.mongo.service.CarService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/car")
@Slf4j
public class CarController {
    private final CarService carService;
    public CarController(CarService carService) {
        this.carService = carService;
    }
    @GetMapping
    public Flux<Car> getCars() {
        return carService.getAllCars();
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Mono<Car> createCar(@RequestBody Car car) {
        return carService.create(car);
    }
    @GetMapping("/{id}")
    public Mono<Car> getCarById(@PathVariable("id") String id) {
        return carService.getCarById(id);
    }
    @DeleteMapping("/{id}")
    public Mono<Void> deleteCar(@PathVariable String id) {
        return carService.deleteCar(id);
    }

    @PutMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public Mono<Car> updateCar(@PathVariable("id") String id, @RequestBody Car car) {
        return carService.updateCar(id, car);
    }
}

CarService

import com.mydaytodo.tutorials.spring.reactive.mongo.model.Car;
import com.mydaytodo.tutorials.spring.reactive.mongo.repository.CarRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Service
@Slf4j
public class CarService {

    @Autowired
    private CarRepository carRepository;
    private final Car TEST_CAR = Car.builder().name("BMW").build();
    private final Car TEST_CAR_2 = Car.builder().name("MG").build();

    public CarService(CarRepository carRepository) {
        this.carRepository = carRepository;
    }

    public Flux<Car> getAllCars() {
        log.info("In the final all cars");
        return carRepository.findAll();
    }
    public Mono<Car> getCarById(String id) {
        return carRepository.findById(id);
    }
    public Mono<Car> getCarByName(String name) {
        return carRepository.findByName(name);
    }

    public Mono<Car> create(Car car) {
        return carRepository.save(car);
    }
    public Mono<Void> deleteCar(String id) {
        return carRepository.deleteById(id);
    }
    public Mono<Car> updateCar(String id, Car car) {
        return carRepository.findById(id)
                .flatMap(existingCar -> {
                    existingCar.setName(car.getName());
                    return carRepository.save(existingCar);
                });
    }
}

CarRepository

import com.mongodb.lang.NonNull;
import com.mydaytodo.tutorials.spring.reactive.mongo.model.Car;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import reactor.core.publisher.Mono;

public interface CarRepository extends ReactiveMongoRepository<Car, String> {

    Mono<Car> findByName(String id);
}

Car Model

import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Document(collection = "cars")
@Builder
public class Car {
    @Id
    private String id;
    private String name;
}

Add some unit tests

  • CarController
import com.mydaytodo.tutorials.spring.reactive.mongo.model.Car;
import com.mydaytodo.tutorials.spring.reactive.mongo.service.CarService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.MockMvc;
import reactor.core.publisher.Mono;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;

@Slf4j
@WebFluxTest(CarController.class)
public class CarControllerTest {
    private MockMvc mockMvc;

    @Autowired
    private WebTestClient webTestClient;
    private static final String TEST_ID = "ISRD_1221";

    @MockBean
    private CarService carService;
    private final Car TEST_CAR = Car.builder().name("BMW").build();
    private final Car TEST_CAR_2 = Car.builder().name("MG").build();

    @BeforeEach
    void setup() {
    }
    @Test
    void testCreateCar() {
        given(carService.create(any())).willReturn(Mono.just(TEST_CAR));
        webTestClient.post().uri("/car")
                .contentType(MediaType.APPLICATION_JSON)
                .bodyValue(TEST_CAR)
                .exchange()
                .expectStatus().isCreated()
                .expectBody(Car.class).isEqualTo(TEST_CAR);
    }
    @Test
    void testGetCarById() {
        given(carService.getCarById(any())).willReturn(Mono.just(TEST_CAR));
        webTestClient.get().uri("/car/"+TEST_ID)
                .exchange()
                .expectStatus().isOk()
                .expectBody(Car.class).isEqualTo(TEST_CAR);
    }
    @Test
    void testDeleteCar() {
        given(carService.getCarById(any())).willReturn(Mono.just(TEST_CAR));
        webTestClient.delete().uri("/car/"+TEST_ID)
                        .exchange()
                    .expectStatus().isOk();
    }
    @Test
    void updateCar() {
        given(carService.updateCar(any(), any())).willReturn(Mono.just(TEST_CAR));
        webTestClient.put().uri("/car/"+TEST_ID)
                .contentType(MediaType.APPLICATION_JSON)
                .bodyValue(TEST_CAR)
                .exchange()
                .expectStatus().isNoContent();
    }
}
  • CarService
import com.mydaytodo.tutorials.spring.reactive.mongo.model.Car;
import com.mydaytodo.tutorials.spring.reactive.mongo.repository.CarRepository;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;


@Slf4j
@ExtendWith(MockitoExtension.class)
public class CarServiceTest {
    @Mock
    private CarRepository carRepository;

    @InjectMocks
    private CarService carService;

    private final Car TEST_CAR = Car.builder().name("BMW").build();
    private final Car TEST_CAR_2 = Car.builder().name("MG").build();
    private static final String TEST_ID = "ISRD_1221";
    private static final String TEST_NAME = "i30";

    @BeforeEach
    public void setup() {
    }
    @Test
    public void testGetCar() {
        when(carRepository.findById(anyString())).thenReturn(Mono.just(Car.builder().name("BMW").build()));
        Mono<Car> carByIdRes = carService.getCarById(TEST_ID);
        StepVerifier.create(carByIdRes)
                .expectNextMatches(car -> car.getName().equals("BMW"))
                .verifyComplete();
    }
    @Test
    public void testGetCarByName() {
        when(carRepository.findByName(anyString())).thenReturn(Mono.just(Car.builder().name("i30").build()));
        Mono<Car> carByIdRes = carService.getCarByName(TEST_NAME);
        StepVerifier.create(carByIdRes)
                .expectNextMatches(car -> car.getName().equals(TEST_NAME))
                .verifyComplete();
    }
    @Test
    public void testCreateCar() {
        when(carRepository.save(any())).thenReturn(Mono.just(TEST_CAR));
        Mono<Car> createCar = carService.create(TEST_CAR);
        StepVerifier.create(createCar)
                .expectNextMatches(car -> car.getName().equals(TEST_CAR.getName()))
                .verifyComplete();
    }

    @Test
    public void testDeleteCar() {
        when(carRepository.deleteById(anyString())).thenReturn(Mono.empty());
        Mono<Void> delRet = carService.deleteCar(TEST_ID);
        StepVerifier.create(delRet)
                .expectComplete()
                .verify();
    }

    @Test
    public void testUpdateCar() {
        when(carRepository.findById(anyString())).thenReturn(Mono.just(TEST_CAR));
        when(carRepository.save(any())).thenReturn(Mono.just(TEST_CAR_2));
        Mono<Car> savedCar = carService.updateCar(TEST_ID, TEST_CAR_2);
        StepVerifier.create(savedCar)
                .expectNextMatches(car -> car.getName().equals(TEST_CAR_2.getName()))
                .verifyComplete();
    }
    @Test
    public void testGetAllCars() {
        when(carRepository.findAll()).thenReturn(Flux.just(TEST_CAR, TEST_CAR_2));
        Flux<Car> carStream = carService.getAllCars();
        StepVerifier.create(carStream)
                .expectNext(TEST_CAR)
                .expectNext(TEST_CAR_2)
                .verifyComplete();
    }
}
How can you see test coverage for your code?

You can do this using this tool called jacoco, which is an open-source Java tool that can help you see what lines of your code are tested. Jacoco is short for say, Java code coverage. This is the plugin you need to add to your pom.xml.

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-surefire-plugin</artifactId>
	<version>2.22.0</version>
</plugin>

<plugin>
	<groupId>org.jacoco</groupId>
	<artifactId>jacoco-maven-plugin</artifactId>
	<version>0.8.7</version>
	<executions>
		<execution>
			<goals>
				<goal>prepare-agent</goal>
			</goals>
		</execution>
		<!-- attached to Maven test phase -->
		<execution>
			<id>report</id>
			<phase>test</phase>
			<goals>
				<goal>report</goal>
			</goals>
		</execution>
	</executions>
</plugin>

How to run this API?

You can get all the source code for this api on Github here. To run this api,

git clone git@github.com:cptdanko/reactive-spring-mongo.git
cd reactive-spring-mongo/reactive.mongo
mvn clean install
mvn spring-boot:run

Conclusion

Reactive systems are here to stay so I hope you found this little tutorial useful and feel more comfortable about building reactive systems after reading this.

If you find any of my posts useful and want to support me, you can buy me a coffee 🙂

https://www.buymeacoffee.com/bhumansoni

While you are here, maybe try one of my apps for the iPhone.

Products – My Day To-Do (mydaytodo.com)

Here are some of my other bloposts on Java

How to build a full stack Spring boot API with ReactJS frontend – My Day To-Do (mydaytodo.com)

How to call REST API with WebClient – My Day To-Do (mydaytodo.com)

How to build a jokes client in Java Spring Boot with RestTemplate – My Day To-Do (mydaytodo.com)

Have a read of some of my other posts on AWS

Upload to AWS S3 bucket from Java Spring Boot app – My Day To-Do (mydaytodo.com)

Deploy NodeJS, Typescript app on AWS Elastic beanstalk – (mydaytodo.com)

How to deploy spring boot app to AWS & serve via https – My Day To-Do (mydaytodo.com)


3 Comments

How to build a blog engine with React & Spring Boot - Part 1 - My Day To-Do · September 21, 2024 at 12:28 am

[…] How to build a Spring Reactive API […]

How to unit test react-redux app - My Day To-Do · September 22, 2024 at 5:43 am

[…] How to build a Spring Reactive API […]

How to build a blog engine with React & Spring Boot – Part 3 CI/CD pipeline · October 4, 2024 at 10:35 pm

[…] How to build a Spring Reactive API […]

Leave a Reply

Avatar placeholder
Verified by MonsterInsights