avatarIvan Franchin

Summary

The provided content is a comprehensive guide on implementing a reactive Book API application using Spring WebFlux, MongoDB, and other modern technologies, detailing the steps to create a scalable and efficient backend system.

Abstract

The article offers a step-by-step tutorial on building a reactive Book API application with Spring WebFlux and MongoDB. It covers the creation of various components such as the model, repository, service, and controller layers, as well as the implementation of DTOs and mappers. The guide emphasizes the benefits of reactive programming, including improved scalability, lower latency, and enhanced fault tolerance. It also includes instructions for setting up the development environment, testing the application's endpoints, and shutting down the services. The tutorial aims to provide a foundation for future articles that will discuss unit and integration testing, caching mechanisms, and other advanced topics related to reactive application development.

Opinions

  • The author advocates for the use of reactive programming in Spring Boot applications to achieve better scalability and performance.
  • The article suggests that using Spring Data Reactive MongoDB optimizes database interactions and aligns with event-driven architecture trends.
  • It is implied that the combination of Spring WebFlux and Netty provides a robust platform for building non-blocking, reactive applications.
  • The author expresses the importance of consistent programming models, which are essential for future-proofing applications.
  • Testing the application's endpoints is presented as a critical step in ensuring the application functions as expected.
  • The use of Docker for running MongoDB is recommended for ease of setup and consistency across different development environments.
  • The author encourages reader engagement and support through social media sharing, following on various platforms, and subscribing to a newsletter for continued learning.

Implementing a Reactive App using Spring WebFlux and MongoDB

Step-by-step guide on how to implement Book API Reactive Spring Boot App

Photo by Tom Hermans on Unsplash

In this article, we will implement a Spring Boot Reactive application, with Spring WebFlux and Netty, called Book API. We will use Spring Data Reactive MongoDB to communicate with the MongoDB database. Book API will expose the following endpoints:

   GET /api/books
   GET /api/books/{id}
  POST /api/books {"title": "...", "author": "...", "year": ...}
 PATCH /api/books/{id} {"title": "...", "author": "...", "year": ...}
DELETE /api/books/{id}

A Spring Boot Reactive app with MongoDB connection provides:

  • Improved scalability through asynchronous, non-blocking operations;
  • Lower latency and resource-efficient processing;
  • Enhanced fault tolerance and higher throughput for concurrent requests;
  • Optimized database interactions with a reactive MongoDB driver;
  • Consistent programming model, ensuring future-proofing and alignment with event-driven architecture trends.

We will use the Book API as the base application in future articles where we will discuss how to implement unit or integration tests for the Book API, how to introduce caching, and more.

Without any further ado, let’s get started.

Prerequisites

If you would like to follow along, you must have Java 17+ and Docker installed on your machine.

Creating Book API Spring-Boot app

Let’s create a Spring-Boot application using Spring Initializr.

The application name will be book-api and the dependencies needed are: Spring Reactive Web, Spring Data Reactive MongoDB, Lombok and Validation.

We will use the Spring Boot version 3.2.0 and Java 17. Here is the link that contains all the setup mentioned previously.

Click the GENERATE button to download a zip file. Unzip the file to a preferred folder and then open the book-api project in your IDE.

Create some packages

In order to keep our code organized, let’s create the following packages, inside com.example.bookapi root package: controller, exception, mapper, model, repository and service.

Create the Model class

In model package, let’s create the Book class with the content below:

package com.example.bookapi.model;

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

@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "books")
public class Book {

    @Id
    private String id;

    private String title;
    private String author;
    private Integer year;

    public Book(String title, String author, Integer year) {
        this.title = title;
        this.author = author;
        this.year = year;
    }
}

The Book class defines the data model for representing books in a MongoDB collection.

Create the Repository interface

In repository package, let’s create the BookRepository interface with the content below:

package com.example.bookapi.repository;

import com.example.bookapi.model.Book;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;

public interface BookRepository extends ReactiveMongoRepository<Book, String> {
}

The BookRepository interface serves as a MongoDB repository for the Book entity, leveraging the Spring Data MongoDB framework. It allows performing various database operations (such as saving, querying, updating, and deleting) on Book entities in a reactive way.

Create the Exception class

In exception package, let’s create the BookNotFoundException class with the content below:

package com.example.bookapi.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND)
public class BookNotFoundException extends RuntimeException {

    public BookNotFoundException(String id) {
        super("Book with id %s not found.".formatted(id));
    }
}

The BookNotFoundException class is thrown when a request is made for a book that doesn’t exist.

Create the Service interface and class

In service package, let’s create the BookService interface with the content below:

package com.example.bookapi.service;

import com.example.bookapi.model.Book;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

public interface BookService {

    Flux<Book> getBooks();

    Mono<Book> validateAndGetBookById(String id);

    Mono<Book> saveBook(Book book);

    Mono<Void> deleteBook(Book book);
}

Also, in service package, let’s create the implementation of the BookService interface:

package com.example.bookapi.service;

import com.example.bookapi.exception.BookNotFoundException;
import com.example.bookapi.model.Book;
import com.example.bookapi.repository.BookRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RequiredArgsConstructor
@Service
public class BookServiceImpl implements BookService {

    private final BookRepository bookRepository;

    @Override
    public Flux<Book> getBooks() {
        return bookRepository.findAll();
    }

    @Override
    public Mono<Book> validateAndGetBookById(String id) {
        return bookRepository.findById(id)
                .switchIfEmpty(Mono.error(new BookNotFoundException(id)));
    }

    @Override
    public Mono<Book> saveBook(Book book) {
        return bookRepository.save(book);
    }

    @Override
    public Mono<Void> deleteBook(Book book) {
        return bookRepository.delete(book);
    }
}

The BookService interface and BookServiceImpl class are part of the service layer. They provide methods for managing books, including retrieving all books, validating and getting a book by ID, saving a book, and deleting a book.

The methods in the BookService and BookServiceImpl are designed to work reactively, utilizing Mono for handling single values and errors, and Flux for handling streams of values.

Create the DTO records

In controller package, let’s create a new package called dto. Inside it, we will create three DTO (Data Transfer Object) records, CreateBookRequest, UpdateBookRequest and BookResponse.

Starting CreateBookRequest record, create it with the content below:

package com.example.bookapi.controller.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;

public record CreateBookRequest(
        @NotBlank String title,
        @NotBlank String author,
        @Positive Integer year) {
}

Then, let’s create the UpdateBookRequest record with the following content:

package com.example.bookapi.controller.dto;

public record UpdateBookRequest(String title, String author, Integer year) {
}

Finally, let’s create the BookResponse record with the content below:

package com.example.bookapi.controller.dto;

import java.io.Serializable;

public record BookResponse(String id, String title, String author, Integer year) implements Serializable {
}

These records are used in the controller class (that will be implemented in the next steps). The CreateBookRequest and the UpdateBookRequest define the expected structure of data in the request bodies for creating and updating book, respectively. The BookResponse defines the expected structure of data in the response.

Create the Mapper interface and class

In mapper package, let’s create the BookMapper interface with the content below:

package com.example.bookapi.mapper;

import com.example.bookapi.controller.dto.BookResponse;
import com.example.bookapi.controller.dto.CreateBookRequest;
import com.example.bookapi.controller.dto.UpdateBookRequest;
import com.example.bookapi.model.Book;

public interface BookMapper {

    Book toBook(CreateBookRequest createBookRequest);

    void updateBookFromUpdateBookRequest(UpdateBookRequest updateBookRequest, Book book);

    BookResponse toBookResponse(Book book);
}

Also, in mapper package, let’s create the implementation of the BookMapper interface:

package com.example.bookapi.mapper;

import com.example.bookapi.controller.dto.BookResponse;
import com.example.bookapi.controller.dto.CreateBookRequest;
import com.example.bookapi.controller.dto.UpdateBookRequest;
import com.example.bookapi.model.Book;
import org.springframework.stereotype.Service;

@Service
public class BookMapperImpl implements BookMapper {

    @Override
    public Book toBook(CreateBookRequest createBookRequest) {
        if (createBookRequest == null) {
            return null;
        }
        Book book = new Book();
        book.setTitle(createBookRequest.title());
        book.setAuthor(createBookRequest.author());
        book.setYear(createBookRequest.year());
        return book;
    }

    @Override
    public void updateBookFromUpdateBookRequest(UpdateBookRequest updateBookRequest, Book book) {
        if (updateBookRequest == null) {
            return;
        }
        if (updateBookRequest.title() != null) {
            book.setTitle(updateBookRequest.title());
        }
        if (updateBookRequest.author() != null) {
            book.setAuthor(updateBookRequest.author());
        }
        if (updateBookRequest.year() != null) {
            book.setYear(updateBookRequest.year());
        }
    }

    @Override
    public BookResponse toBookResponse(Book book) {
        if (book == null) {
            return null;
        }
        return new BookResponse(book.getId(), book.getTitle(), book.getAuthor(), book.getYear());
    }
}

The BookMapper interface and BookMapperImpl class are responsible to perform some mapping from the DTO classes CreateBookRequest and UpdateBookRequest to the Book MongoDB document class. Also, to map from Book to BookResponse.

Create the Controller class

In controller package, let’s create the BookController class with the content below:

package com.example.bookapi.controller;

import com.example.bookapi.controller.dto.BookResponse;
import com.example.bookapi.controller.dto.CreateBookRequest;
import com.example.bookapi.controller.dto.UpdateBookRequest;
import com.example.bookapi.mapper.BookMapper;
import com.example.bookapi.model.Book;
import com.example.bookapi.service.BookService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RequiredArgsConstructor
@RestController
@RequestMapping("/api/books")
public class BookController {

    private final BookService bookService;
    private final BookMapper bookMapper;

    @GetMapping(produces = MediaType.APPLICATION_NDJSON_VALUE)
    public Flux<BookResponse> getBook() {
        return bookService.getBooks().map(bookMapper::toBookResponse);
    }

    @GetMapping("/{id}")
    public Mono<BookResponse> getBook(@PathVariable String id) {
        return bookService.validateAndGetBookById(id).map(bookMapper::toBookResponse);
    }

    @ResponseStatus(HttpStatus.CREATED)
    @PostMapping
    public Mono<BookResponse> createBook(@Valid @RequestBody CreateBookRequest createBookRequest) {
        Book book = bookMapper.toBook(createBookRequest);
        return bookService.saveBook(book).map(bookMapper::toBookResponse);
    }

    @PatchMapping("/{id}")
    public Mono<BookResponse> updateBook(@PathVariable String id,
                                         @RequestBody UpdateBookRequest updateBookRequest) {
        return bookService.validateAndGetBookById(id)
                .doOnSuccess(book -> {
                    bookMapper.updateBookFromUpdateBookRequest(updateBookRequest, book);
                    bookService.saveBook(book).subscribe();
                })
                .map(bookMapper::toBookResponse);
    }

    @DeleteMapping("/{id}")
    public Mono<BookResponse> deleteBook(@PathVariable String id) {
        return bookService.validateAndGetBookById(id)
                .doOnSuccess(book -> bookService.deleteBook(book).subscribe())
                .map(bookMapper::toBookResponse);
    }
}

The BookControllerclass is a Spring WebFlux controller that handles HTTP requests related to books in a reactive style using Mono and Flux from the Reactor library. It provides endpoints to get, create, update, and delete books using bookService and bookMapper.

Update the application.properties

Let’s update the application.properties file with the content below:

spring.application.name=book-api

spring.data.mongodb.uri=mongodb://${MONGODB_HOST:localhost}:${MONGODB_PORT:27017}/bookdb

logging.level.org.springframework.data.mongodb.core=DEBUG

A shorter explanation of the properties:

  • spring.application.name: Specifies the name of the application;
  • spring.data.mongodb.uri: Specifies the connection URI for MongoDB;
  • logging.level.org.springframework.data.mongodb.core: Sets the logging level for the Spring Data MongoDB core component.

Starting Environment

Start MongoDB

In a terminal, run the following command to start the MongoDB Docker container:

docker run -d --name mongodb -p 27017:27017 mongo:7.0.4

Start Book API

Next, make sure you and inside the book-api root folder. Then, run the command below to start Book API:

./mvnw clean spring-boot:run

Testing Endpoints

In a terminal, let’s run the following cURL commands to interact with Book API endpoints.

Let’s retrieve all books:

curl -i localhost:8080/api/books

As there is no book registered, we should get:

HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: application/x-ndjson

Let’s create a book:

curl -i -X POST localhost:8080/api/books \
  -H 'Content-Type: application/json' \
  -d '{"title": "Spring in Action", "author": "Craig Walls and Ryan Breidenbach", "year": 1005}'

It should return:

HTTP/1.1 201 Created
Content-Type: application/json
...
{"id":"657622259ba4516799bfc290","title":"Spring in Action","author":"Craig Walls and Ryan Breidenbach","year":1005}

Note: In my case, the book ID is 657622259ba4516799bfc290. For you, it will be different. In order to facilitate the writing of this article, let’s export to an environment variable the book ID. For it, run the following command in the terminal where you are performing the calls to Book API endpoints:

BOOK_ID=<the-created-book-id>

Again, let’s retrieve all books:

curl -i localhost:8080/api/books

This time, it should return:

HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: application/x-ndjson

{"id":"657622259ba4516799bfc290","title":"Spring in Action","author":"Craig Walls and Ryan Breidenbach","year":1005}

Let’s update the book year, as it should be 2005 and not 1005:

curl -i -X PATCH localhost:8080/api/books/$BOOK_ID \
  -H 'Content-Type: application/json' \
  -d '{"year": 2005}'

We should get:

HTTP/1.1 200 OK
Content-Type: application/json
...
{"id":"657622259ba4516799bfc290","title":"Spring in Action","author":"Craig Walls and Ryan Breidenbach","year":2005}

Let’s retrieve the book:

curl -i localhost:8080/api/books/$BOOK_ID

We should get as response:

HTTP/1.1 200 OK
Content-Type: application/json
...
{"id":"657622259ba4516799bfc290","title":"Spring in Action","author":"Craig Walls and Ryan Breidenbach","year":2005}

Finally, let’s delete the book:

curl -i -X DELETE localhost:8080/api/books/$BOOK_ID

The response should be:

HTTP/1.1 200 OK
Content-Type: application/json
...
{"id":"657622259ba4516799bfc290","title":"Spring in Action","author":"Craig Walls and Ryan Breidenbach","year":2005}

In case, we try to retrieve a book that does not exist:

curl -i localhost:8080/api/books/$BOOK_ID

The following response should be returned:

HTTP/1.1 404 Not Found
Content-Type: application/json
...
{"timestamp":"2023-12-10T20:42:54.044+00:00","path":"/api/books/657622259ba4516799bfc290","status":404,"error":"Not Found","requestId":"f41a3c79-8"}

Shutdown

In the terminal where the Book API is running, press Ctrl+C to stop the application.

To stop the MongoDB Docker container, run the following command in a terminal:

docker rm -fv mongodb

Conclusion

In this article, we created a Spring Boot Reactive application named Book API, employing Spring WebFlux and Netty, and establishing a connection to MongoDB via Spring Data Reactive MongoDB.

This reactive Spring Boot app coupled with MongoDB brings several advantages: enhanced scalability achieved through asynchronous, non-blocking operations; reduced latency and resource-efficient processing; optimized database interactions facilitated by a reactive MongoDB driver; and more.

To ensure the application performs as anticipated, we conclude by testing its endpoints for a seamless and expected execution.

Additional Readings

Support and Engagement

If you enjoyed this article and would like to show your support, please consider taking the following actions:

  • 👏 Engage by clapping, highlighting, and replying to my story. I’ll be happy to answer any of your questions;
  • 🌐 Share my story on Social Media;
  • 🔔 Follow me on: Medium | LinkedIn | Twitter | GitHub;
  • ✉️ Subscribe to my newsletter, so you don’t miss out on my latest posts.
Reactive
Spring Boot
Technology
Software Development
Webflux
Recommended from ReadMedium