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.
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.
In the next tutorial, we will see how to use a functional-style programming model to build reactive CRUD REST APIs.
Check out all Spring boot tutorials at https://www.javaguides.net/p/spring-boot-tutorial.html
Check out all Microservices tutorials at https://www.javaguides.net/p/spring-boot-microservices-tutorial.html
Comments
Post a Comment
Leave Comment