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.Mono and Flux
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
Programming models supported by Spring WebFlux
- 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.
Reactive MongoDB Driver
1. 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
spring.data.mongodb.uri=mongodb://localhost:27017/ems
4. Create Domain Class
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;
}
5. Creating Repository - EmployeeRepository
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> {
}
6. Create EmployeeDTO and EmployeeMapper - Map Entity to Dto and Vice Versa
EmployeeDto
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
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
EmployeeService Interface
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
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
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);
}
}
9. Testing Reactive CRUD REST APIs using WebClientTest Class
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);
}
}
Comments
Post a Comment
Leave Comment