November 20

By Joshua Matos

Asynchronous Programming, Reactive Programming, Software Development, Spring WebFlux, Testing WebFlux

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:

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());
    }
}

  1. The CoffeeController class is annotated with @RestController to indicate that it's a Spring MVC controller that handles RESTful requests.

  2. The constructor injection is used to inject the CoffeeService into the controller, allowing it to delegate business logic to the service.

  3. Various HTTP request mapping annotations (@GetMapping, @PostMapping, @PutMapping, @DeleteMapping) define the RESTful API endpoints.
     
  4. For example, @GetMapping("/api/coffee") maps the getAllCoffees method to handle GET requests to "/api/coffees."

  5. The @PathVariable annotation is used to extract values from the URL path.

  6. The @RequestBody annotation is used to map the request body to a Coffee object when creating or updating a coffee.

  7. 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


>
Share via
Copy link