Spring WebFlux Reactive CRUD REST API Example

In this tutorial, we will learn how to build a reactive CRUD REST API using Spring Boot, Spring WebFlux, MongoDB, Lombok, and IntelliJ IDEA. 

CRUD stands for "create, read, update, and delete," which are the four basic functions of persistent storage. Spring WebFlux is a non-blocking, reactive web framework for building reactive, scalable web applications.  Together, Spring WebFlux and CRUD can be used to quickly develop a reactive RESTful API that can create, read, update, and delete data in a database.

Spring WebFlux

Spring WebFlux is a non-blocking, reactive web framework for building reactive, scalable web applications. It is part of the Spring Framework, and it is built on top of Project Reactor, which is a reactive programming library for building asynchronous, non-blocking applications.

Overall, Spring WebFlux is a powerful tool for building reactive, scalable web applications, and it is well-suited for use in high-concurrency environments.

Mono and Flux

Spring WebFlux majorly uses two publishers: Mono and Flux
Mono: Returns 0 or 1 element.
The Mono API allows producing only one value.
Flux: Returns 0…N elements.
The Flux can be endless, it can produce multiple values.

Mono vs Flux

Mono and Flux are both implementations of the Publisher interface. In simple terms, we can say that when we're doing something like a computation or making a request to a database or an external service, and expecting a maximum of one result, then we should use Mono.

When we're expecting multiple results from our computation, database, or external service call, then we should use Flux.

Programming models supported by Spring WebFlux 

Spring WebFlux supports two types of programming models: 
  • The traditional annotation-based model with @Controller, @RestController, @RequestMapping, and other annotations that you have been using in Spring MVC. 
  • A brand new Functional style model based on Java 8 lambdas for routing and handling requests.
In this tutorial, We’ll be using the traditional annotation-based programming model. 

Reactive MongoDB Driver

We’ll use MongoDB as our data store along with the reactive MongoDB driver. 

Reactive MongoDB is a driver for MongoDB that is designed to support the Reactive Streams specification. It allows developers to build reactive, non-blocking applications that can work with MongoDB in a more efficient and scalable manner.

1. Maven Dependencies

Make sure to add the following Maven dependencies:
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-webflux</artifactId>
		</dependency>

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>io.projectreactor</groupId>
			<artifactId>reactor-test</artifactId>
			<scope>test</scope>
		</dependency>

2. Project Structure

Refer to the below screenshot to create the packing or project structure for the application:

3. Configure MongoDB

You can configure MongoDB by simply adding the following property to the application.properties file:
spring.data.mongodb.uri=mongodb://localhost:27017/ems
Spring Boot will read this configuration on startup and automatically configure the data source.

4. Create Domain Class

Let's create an Employee MongoDB document and add the following content to it:
package net.javaguides.springbootwebfluxdemo.entity;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Document(value = "employees")
public class Employee {

    @Id
    private String id;
    private String firstName;
    private String lastName;
    private String email;
}
Note that we have given MongoDB collection name - employees.

5. Creating Repository - EmployeeRepository

Next, we’re going to create the data access layer which will be used to access the MongoDB database.

Let's create an EmployeeRepository interface and add the following content to it:
package net.javaguides.springbootwebfluxdemo.repository;

import net.javaguides.springbootwebfluxdemo.entity.Employee;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;

public interface EmployeeRepository extends ReactiveCrudRepository<Employee, String> {
}
The EmployeeRepository interface extends from ReactiveMongoRepository which exposes various CRUD methods on the Document. Spring Boot automatically plugs in an implementation of this interface called SimpleReactiveMongoRepository at runtime.

So you get all the CRUD methods on the Document readily available to you without needing to write any code.

6. Create EmployeeDTO and EmployeeMapper - Map Entity to Dto and Vice Versa

EmployeeDto

Let's create EmployeeDto to transfer the data between the client and server:
package net.javaguides.springbootwebfluxdemo.dto;

import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class EmployeeDto {
    private String id;
    private String firstName;
    private String lastName;
    private String email;
}

EmployeeMapper - Map Entity to Dto and Vice Versa

Let's create EmployeeMapper class to map an entity to Dto and vice versa:
package net.javaguides.springbootwebfluxdemo.mapper;

import net.javaguides.springbootwebfluxdemo.dto.EmployeeDto;
import net.javaguides.springbootwebfluxdemo.entity.Employee;

public class EmployeeMapper {

    public static EmployeeDto mapToEmployeeDto(Employee employee){
        return new EmployeeDto(
                employee.getId(),
                employee.getFirstName(),
                employee.getLastName(),
                employee.getEmail()
        );
    }

    public static Employee mapToEmployee(EmployeeDto employeeDto){
        return new Employee(
                employeeDto.getId(),
                employeeDto.getFirstName(),
                employeeDto.getLastName(),
                employeeDto.getEmail()
        );
    }
}

7. Create a Service Layer

This layer will contain the business logic for the API and will be used to perform CRUD operations using the Repository.

EmployeeService Interface

Let's create an EmployeeService interface and add below CRUD methods to it:
package net.javaguides.springbootwebfluxdemo.service;

import net.javaguides.springbootwebfluxdemo.dto.EmployeeDto;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

public interface EmployeeService {
    Mono<EmployeeDto> saveEmployee(EmployeeDto employeeDto);

    Mono<EmployeeDto> getEmployee(String employeeId);

    Flux<EmployeeDto> getAllEmployees();

    Mono<EmployeeDto> updateEmployee(EmployeeDto employeeDto, String employeeId);

    Mono<Void> deleteEmployee(String employeeId);
}

EmployeeServiceImpl class

Let's create EmployeeServiceImpl class that implements the EmployeeService interface and its methods:
package net.javaguides.springbootwebfluxdemo.service.impl;

import lombok.AllArgsConstructor;
import net.javaguides.springbootwebfluxdemo.dto.EmployeeDto;
import net.javaguides.springbootwebfluxdemo.entity.Employee;
import net.javaguides.springbootwebfluxdemo.mapper.EmployeeMapper;
import net.javaguides.springbootwebfluxdemo.repository.EmployeeRepository;
import net.javaguides.springbootwebfluxdemo.service.EmployeeService;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Service
@AllArgsConstructor
public class EmployeeServiceImpl implements EmployeeService {

    private EmployeeRepository employeeRepository;

    @Override
    public Mono<EmployeeDto> saveEmployee(EmployeeDto employeeDto) {
        Employee employee = EmployeeMapper.mapToEmployee(employeeDto);
        Mono<Employee> savedEmployee = employeeRepository.save(employee);
        return savedEmployee
                .map((employeeEntity) -> EmployeeMapper.mapToEmployeeDto(employeeEntity));
    }

    @Override
    public Mono<EmployeeDto> getEmployee(String employeeId) {
        Mono<Employee> employeeMono = employeeRepository.findById(employeeId);
        return employeeMono.map((employee -> EmployeeMapper.mapToEmployeeDto(employee)));
    }

    @Override
    public Flux<EmployeeDto> getAllEmployees() {

        Flux<Employee> employeeFlux  = employeeRepository.findAll();
        return employeeFlux
                .map((employee) -> EmployeeMapper.mapToEmployeeDto(employee))
                .switchIfEmpty(Flux.empty());
    }

    @Override
    public Mono<EmployeeDto> updateEmployee(EmployeeDto employeeDto, String employeeId) {

        Mono<Employee> employeeMono = employeeRepository.findById(employeeId);

        return employeeMono.flatMap((existingEmployee) -> {
            existingEmployee.setFirstName(employeeDto.getFirstName());
            existingEmployee.setLastName(employeeDto.getLastName());
            existingEmployee.setEmail(employeeDto.getEmail());
            return employeeRepository.save(existingEmployee);
        }).map((employee -> EmployeeMapper.mapToEmployeeDto(employee)));
    }

    @Override
    public Mono<Void> deleteEmployee(String employeeId) {
        return employeeRepository.deleteById(employeeId);
    }
}

8. Create Controller Layer - Reactive REST APIs

Now, we’ll build REST APIs for creating, retrieving, updating, and deleting an Employee. All the REST APIs will be asynchronous and will return a Publisher (Mono or Flux).

Let's create an EmployeeController with the following contents:
package net.javaguides.springbootwebfluxdemo.controller;

import lombok.AllArgsConstructor;
import net.javaguides.springbootwebfluxdemo.dto.EmployeeDto;
import net.javaguides.springbootwebfluxdemo.service.EmployeeService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("api/employees")
@AllArgsConstructor
public class EmployeeController {

    private EmployeeService employeeService;

    @PostMapping
    @ResponseStatus(value = HttpStatus.CREATED)
    public Mono<EmployeeDto> saveEmployee(@RequestBody EmployeeDto employeeDto){
        return employeeService.saveEmployee(employeeDto);
    }

    @GetMapping("{id}")
    public Mono<EmployeeDto> getEmployee(@PathVariable("id") String employeeId){
        return employeeService.getEmployee(employeeId);
    }

    @GetMapping
    public Flux<EmployeeDto> getAllEmployees(){
        return employeeService.getAllEmployees();
    }

    @PutMapping("{id}")
    public Mono<EmployeeDto> updateEmployee(@RequestBody EmployeeDto employeeDto,
                                            @PathVariable("id") String employeeId){
        return employeeService.updateEmployee(employeeDto, employeeId);
    }

    @DeleteMapping("{id}")
    @ResponseStatus(value = HttpStatus.NO_CONTENT)
    public Mono<Void> deleteEmployee(@PathVariable("id") String employeeId){
        return employeeService.deleteEmployee(employeeId);
    }
}
Note that all the controller endpoints return a Publisher in the form of a Flux or a Mono.

9. Testing Reactive CRUD REST APIs using WebClientTest Class

Let's write the Integration test cases to test CRUD REST APIs using the WebTestClient class.

We are using the below WebTestClient class method to prepare CRUD REST API requests:
post() - Prepare an HTTP POST request.
delete() - Prepare an HTTP DELETE request. 
get() - Prepare an HTTP GET request. 
put() - Prepare an HTTP PUT request.

Here is the complete code for testing Spring WebFlux Reactive CRUD Rest APIs using WebTestClient:
package net.javaguides.springboot;

import net.javaguides.springboot.dto.EmployeeDto;
import net.javaguides.springboot.repository.EmployeeRepository;
import net.javaguides.springboot.service.EmployeeService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.Collections;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class EmployeeControllerIntegrationTests {

    @Autowired
    private EmployeeService employeeService;

    @Autowired
    private WebTestClient webTestClient;

    @Autowired
    private EmployeeRepository employeeRepository;

    @BeforeEach
    public void before(){
        System.out.println("Before Each Test");
        employeeRepository.deleteAll().subscribe();
    }

    @Test
    public void testSaveEmployee(){

        EmployeeDto employeeDto = new EmployeeDto();
        employeeDto.setFirstName("John");
        employeeDto.setLastName("Cena");
        employeeDto.setEmail("john@gmail.com");

        webTestClient.post().uri("/api/employees")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .body(Mono.just(employeeDto), EmployeeDto.class)
                .exchange()
                .expectStatus().isCreated()
                .expectBody()
                .consumeWith(System.out::println)
                .jsonPath("$.firstName").isEqualTo(employeeDto.getFirstName())
                .jsonPath("$.lastName").isEqualTo(employeeDto.getLastName())
                .jsonPath("$.email").isEqualTo(employeeDto.getEmail());
    }

    @Test
    public void testGetSingleEmployee(){

        EmployeeDto employeeDto = new EmployeeDto();
        employeeDto.setFirstName("Meena");
        employeeDto.setLastName("Fadatare");
        employeeDto.setEmail("meena@gmail.com");

        EmployeeDto savedEmployee = employeeService.saveEmployee(employeeDto).block();

        webTestClient.get().uri("/api/employees/{id}", Collections.singletonMap("id",savedEmployee.getId()))
                .exchange()
                .expectStatus().isOk()
                .expectBody()
                .consumeWith(System.out::println)
                .jsonPath("$.id").isEqualTo(savedEmployee.getId())
                .jsonPath("$.firstName").isEqualTo(employeeDto.getFirstName())
                .jsonPath("$.lastName").isEqualTo(employeeDto.getLastName())
                .jsonPath("$.email").isEqualTo(employeeDto.getEmail());
    }

    @Test
    public void testGetAllEmployees(){

        EmployeeDto employeeDto = new EmployeeDto();
        employeeDto.setFirstName("John");
        employeeDto.setLastName("Cena");
        employeeDto.setEmail("john@gmail.com");

        employeeService.saveEmployee(employeeDto).block();

        EmployeeDto employeeDto1 = new EmployeeDto();
        employeeDto1.setFirstName("Meena");
        employeeDto1.setLastName("Fadatare");
        employeeDto1.setEmail("meena@gmail.com");

        employeeService.saveEmployee(employeeDto1).block();

        webTestClient.get().uri("/api/employees")
                .accept(MediaType.APPLICATION_JSON)
                .exchange()
                .expectStatus().isOk()
                .expectBodyList(EmployeeDto.class)
                .consumeWith(System.out::println);
    }

    @Test
    public void testUpdateEmployee(){

        EmployeeDto employeeDto = new EmployeeDto();
        employeeDto.setFirstName("Ramesh");
        employeeDto.setLastName("Fadatare");
        employeeDto.setEmail("ramesh@gmail.com");

        EmployeeDto savedEmployee = employeeService.saveEmployee(employeeDto).block();

        EmployeeDto updatedEmployee = new EmployeeDto();
        updatedEmployee.setFirstName("Ram");
        updatedEmployee.setLastName("Jadhav");
        updatedEmployee.setEmail("ram@gmail.com");

        webTestClient.put().uri("/api/employees/{id}", Collections.singletonMap("id", savedEmployee.getId()))
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .body(Mono.just(updatedEmployee), EmployeeDto.class)
                .exchange()
                .expectStatus().isOk()
                .expectBody()
                .consumeWith(System.out::println)
                .jsonPath("$.firstName").isEqualTo(updatedEmployee.getFirstName())
                .jsonPath("$.lastName").isEqualTo(updatedEmployee.getLastName())
                .jsonPath("$.email").isEqualTo(updatedEmployee.getEmail());
    }

    @Test
    public void testDeleteEmployee(){

        EmployeeDto employeeDto = new EmployeeDto();
        employeeDto.setFirstName("Ramesh");
        employeeDto.setLastName("Fadatare");
        employeeDto.setEmail("ramesh@gmail.com");

        EmployeeDto savedEmployee = employeeService.saveEmployee(employeeDto).block();

        webTestClient.delete().uri("/api/employees/{id}", Collections.singletonMap("id", savedEmployee.getId()))
                .exchange()
                .expectStatus().isNoContent()
                .expectBody()
                .consumeWith(System.out::println);

    }
}

Output:

Here is the output of all the JUnit test cases:

Conclusion

In this tutorial, we have seen how to use a traditional annotation-based programming model to build reactive CRUD REST APIs.


Check out all Spring boot tutorials at https://www.javaguides.net/p/spring-boot-tutorial.html

Comments