Major companies using WebFlux:
Building a Reactive Coffee App with React
Let's begin with our guide on creating a reactive application using Spring WebFlux with Spring Boot.
This tutorial will walk you through setting up a basic Spring Boot project, building a reactive RESTful API to manage coffee, and using R2DBC for database interactions.
Our Controller will return a Flux or a Mono, reactive types. These are used for asynchronous processing. Mono represents a single or zero element, and Flux represents a stream of elements.
Prerequisites
Before diving in, make sure you have the following:
- Java 17 or later
- Maven or Gradle (we'll use Maven in this tutorial)
- A favorite IDE (like IntelliJ IDEA, Eclipse, etc.)
- Basic understanding of Spring Boot and reactive programming
Step 1: Setting Up the Project
Create a new Spring Boot project using Spring Initializr. Select the following dependencies:
- Spring Reactive Web (WebFlux)
- R2DBC
- Lombok
- PostgreSQL Driver
Additional:
- Download and Install Docker
Generate and download the project. Unzip and open it in your IDE.
Step 2: Setting up Docker-compose YAML
- Create a
docker-compose.yml
file in your project directory. - Define Docker containers for your PostgreSQL database and any other necessary services.
- Configure environment variables, ports, and volumes as needed for your services.
- Run
docker-compose up
to start the Docker containers.
services:
coffee_db:
image: postgres:latest
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
Step 3: Setting up Application YAML
spring:
application:
name: coffee-shop
r2dbc:
url: r2dbc:postgresql://localhost:5432/postgres
username: postgres
password: postgres
server:
port: 8080
This YAML configuration file sets up your Spring WebFlux application with the following configurations:
Application Name: The name of your Spring application is set to "coffee-shop."
Server Port: The application runs on port 8080 by default. You can change it if needed.
Database Configuration: Configure the connection details for your PostgreSQL database under the "spring.r2dbc" section.
We used postgres as it's the defaults, make sure these settings match any changes you made in your docker file.
Step 4: Setting up CoffeeEntity
@Data @Table
public class Coffee {
@Id
private Long id;
private String name;
private String roast;
private Double price;
}
The @Table annotation specifies the name of the table in the database where the Coffee entities will be stored. The @Id annotation marks the id field as the primary key.
We use @Data to enable the getters and setters on the entity.
If you are familiar with extending the JpaRepository. You'll notice that we've omitted the @Entity annotation. It's not needed with the R2dbcRepository interface.
Step 5: Setting up the R2DBC Repository
In this step, you'll set up a repository to interact with your Coffee entity using R2DBC, a reactive database access library.
R2DBC allows you to perform database operations asynchronously and non-blocking, which aligns well with the reactive programming model of Spring WebFlux.
Normally if you were using a JpaRepository, each request would block. Slowind down the rest of the application.
Here's an example of how you can create a CoffeeRepository interface:
public interface CoffeeRepository extends R2dbcRepository<Coffee, Long> {
}
CoffeeRepository extends R2dbcRepository, a Spring Data R2DBC repository interface. It provides basic CRUD (Create, Read, Update, Delete) operations for the Coffee entity.
The <Coffee, Long> parameters specify the entity type and the primary key type (Long in this case).
You can define custom query methods in this interface if you need more complex database queries.
With the repository in place, you can proceed to the following steps, such as setting up the Coffee service and controller to handle the business logic and RESTful API endpoints for your Coffee entity.
Step 6: Setting up Coffee Service
@Service
@RequiredArgsConstructor
public class CoffeeService {
private final CoffeeRepository coffeeRepository;
public Flux<Coffee> getAllCoffee() {
return coffeeRepository.findAll();
}
public Mono<Coffee> getCoffeeById(Long id) {
return coffeeRepository.findById(id);
}
public Mono<Coffee> createCoffee(Coffee coffee) {
return coffeeRepository.save(coffee);
}
public Mono<Coffee> updateCoffee(Long id, Coffee coffee) {
return coffeeRepository.findById(id)
.flatMap(existingCoffee -> {
existingCoffee.setName(coffee.getName());
existingCoffee.setRoast(coffee.getRoast());
existingCoffee.setPrice(coffee.getPrice());
return coffeeRepository.save(existingCoffee);
});
}
public Mono<Void> deleteCoffee(Long id) {
return coffeeRepository.deleteById(id);
}
}
The CoffeeService class is annotated with @Service to indicate that it's a Spring-managed service component. We will be injecting this service later into our controller.
The constructor injection is used to inject the CoffeeRepository into the service. This allows the service to interact with the database.
The CoffeeService provides methods to perform CRUD operations on the Coffee entity, such as getAllCoffees, getCoffeeById, createCoffee, updateCoffee, and deleteCoffee.
When updating a coffee (updateCoffee method), the existing coffee is retrieved, and its properties are updated with the new values before saving it back to the database.
Step 7: Setting up Coffee Controller
@RestController
@RequestMapping("/api/coffee")
@RequiredArgsConstructor
public class CoffeeController {
private final CoffeeService coffeeService;
@GetMapping
public Mono<ResponseEntity<List<Coffee>>> getAllCoffee() {
return coffeeService.getAllCoffee()
.collectList()
.map(ResponseEntity::ok);
}
@GetMapping("/{id}")
public Mono<ResponseEntity<Coffee>> getCoffeeById(@PathVariable Long id) {
return coffeeService.getCoffeeById(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@PostMapping
public Mono<ResponseEntity<Coffee>> createCoffee(@RequestBody Coffee coffee) {
return coffeeService.createCoffee(coffee)
.map(c -> ResponseEntity.status(HttpStatus.CREATED).body(c));
}
@PutMapping("/{id}")
public Mono<ResponseEntity<Coffee>> updateCoffee(@PathVariable Long id, @RequestBody Coffee coffee) {
return coffeeService.updateCoffee(id, coffee)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public Mono<ResponseEntity<Void>> deleteCoffee(@PathVariable Long id) {
return coffeeService.deleteCoffee(id)
.then(Mono.just(ResponseEntity.ok().<Void>build()))
.defaultIfEmpty(ResponseEntity.notFound().build());
}
}
- The CoffeeController class is annotated with @RestController to indicate that it's a Spring MVC controller that handles RESTful requests.
- The constructor injection is used to inject the CoffeeService into the controller, allowing it to delegate business logic to the service.
- Various HTTP request mapping annotations (@GetMapping, @PostMapping, @PutMapping, @DeleteMapping) define the RESTful API endpoints.
- For example, @GetMapping("/api/coffee") maps the getAllCoffees method to handle GET requests to "/api/coffees."
- The @PathVariable annotation is used to extract values from the URL path.
- The @RequestBody annotation is used to map the request body to a Coffee object when creating or updating a coffee.
- The controller methods return ResponseEntity objects, which allow you to set HTTP status codes and wrap the response data.
Step 8: Test your application
Let's make sure it works all together. The good thing here is that we use a tool called WebTestClient. It helps us simulate and test our web requests and responses.
With WebTestClient, we create mock requests to our endpoints, check out the responses, and ensure everything behaves as expected.
It's different from the usual testing you might be used to.
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
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.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@WebFluxTest(CoffeeController.class)
class CoffeeControllerTest {
@Autowired
private WebTestClient webTestClient;
@MockBean
private CoffeeService coffeeService;
@Test
void getAllCoffee() {
List<Coffee> coffeeList = Arrays.asList(
new Coffee(1L, "Latte", "Medium", 3.5),
new Coffee(2L, "Espresso", "Dark", 2.5)
);
Mockito.when(coffeeService.getAllCoffee())
.thenReturn(Flux.fromIterable(coffeeList));
webTestClient.get().uri("/api/coffee")
.exchange()
.expectStatus().isOk()
.expectBodyList(Coffee.class).isEqualTo(coffeeList);
}
@Test
void getCoffeeById() {
Coffee coffee = new Coffee(1L, "Latte", "Medium", 3.5);
Mockito.when(coffeeService.getCoffeeById(1L))
.thenReturn(Mono.just(coffee));
webTestClient.get().uri("/api/coffee/{id}", 1L)
.exchange()
.expectStatus().isOk()
.expectBody(Coffee.class).isEqualTo(coffee);
}
@Test
void createCoffee() {
Coffee coffee = new Coffee(null, "Cappuccino", "Medium", 4.0);
Coffee savedCoffee = new Coffee(1L, "Cappuccino", "Medium", 4.0);
Mockito.when(coffeeService.createCoffee(coffee))
.thenReturn(Mono.just(savedCoffee));
webTestClient.post().uri("/api/coffee")
.bodyValue(coffee)
.exchange()
.expectStatus().isCreated()
.expectBody(Coffee.class).isEqualTo(savedCoffee);
}
@Test
void updateCoffee() {
Coffee existingCoffee = new Coffee(1L, "Latte", "Medium", 3.5);
Coffee updatedCoffee = new Coffee(1L, "Latte", "Dark", 3.5);
Mockito.when(coffeeService.updateCoffee(1L, updatedCoffee))
.thenReturn(Mono.just(updatedCoffee));
webTestClient.put().uri("/api/coffee/{id}", 1L)
.bodyValue(updatedCoffee)
.exchange()
.expectStatus().isOk()
.expectBody(Coffee.class).isEqualTo(updatedCoffee);
}
@Test
void deleteCoffee() {
Mockito.when(coffeeService.deleteCoffee(1L))
.thenReturn(Mono.empty());
webTestClient.delete().uri("/api/coffee/{id}", 1L)
.exchange()
.expectStatus().isOk();
}
}
I hope you found this tutorial helpful, if you have be sure to subscribe to the newsletter and consider subscribing to the YouTube Channel @JoshuaMatosDev.