Developing a REST API using Spring WebFlux

This tutorial is the fifth in a series on Reactive Programming in Java and Spring Framework. In this tutorial, we will develop a simple REST API using Spring Web flux. To completely understand this tutorial, it is better to read a previous tutorial first. It would also help to know how to develop a REST Controller using Spring Framework in a Blocking (Non-reactive) way.

The Web Reactive Stack

The image below from Spring Webflux documentation shows how the Spring Web Reactive Stack is different from and similar to the Spring MVC’s Stack.

Webflux and MVC Stack

Hands-On-Code

The REST API we will build is a simple CRUD API that will be responsible for interacting with books stored in MongoDB. We will use Maven as a Dependency Management tool. Below is the project structure:

Below is the pom.xml file I used for the project:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.sergey</groupId>
    <artifactId>reactive_api</artifactId>
    <version>0.0.1</version>
    <name>reactive_api</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
        </dependency>
        <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>de.flapdoodle.embed</groupId>
            <artifactId>de.flapdoodle.embed.mongo</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

To reduce configurations, we will use an in-memory MongoDB. This will be provided by the flapdoodle project library.

Model

The Book Entity will be the single entity in the model. For simplicity, we will consider each book to have a single author.

@Document
@Data
@NoArgsConstructor
public class Book {
    @Id
    private String id;
    private String name;
    private String author;

    public Book(String name, String author) {
        this.name = name;
        this.author = author;
    }
}

The Lombok Project has been used to reduce boilerplate code.

Service

I am a big fan of SOLID principles, so we will make everything depend on abstractions. There will be an interface for the service and implementation of this service.

Interface

public interface BookService {
    Mono<Book> getBookById(String id);

    Flux<Book> getAllBooks();

    Mono<Void> deleteBookById(String id);

    Mono<Void> deleteAllBooks();

    Mono<Book> createBook(Book book);

    Mono<Book> updateBook(Book book);
}

Implementation

@Service
public class BookServiceImpl implements BookService {
    private final BookRepository bookRepository;

    public BookServiceImpl(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    private Book formatBook(Book book) {
        book.setName(book.getName().toLowerCase());
        book.setAuthor(book.getAuthor().toUpperCase());
        return book;
    }

    @Override
    public Mono<Book> getBookById(String id) {
        return bookRepository.findById(id)
                .map(this::formatBook);
    }

    @Override
    public Flux<Book> getAllBooks() {
        return bookRepository.findAll().map(this::formatBook);
    }

    @Override
    public Mono<Void> deleteBookById(String id) {
        return bookRepository.deleteById(id);
    }

    @Override
    public Mono<Void> deleteAllBooks() {
        return bookRepository.deleteAll();
    }

    @Override
    public Mono<Book> createBook(Book book) {
        if (book.getId() != null) {
            return Mono.error(new IllegalArgumentException("Id of New Book Must be Null"));
        }
        return bookRepository.save(book);
    }

    @Override
    public Mono<Book> updateBook(Book book) {
      return  bookRepository.existsById(book.getId())
                .flatMap(isExisting ->{
                    if (isExisting) {
                        return bookRepository.save(book);
                    } else {
                        return Mono.error(new IllegalArgumentException("The Book Id must exist for Update To Occur"));
                    }
                });
    }
}

More logic may be added in the implementation class if required.

Rest Controller

When using Spring Webflux, there are two ways to make a controller:

1) Using annotations as in Spring MVC.

This is the simplest method for those with a strong Spring MVC background. Below is the Controller class with all the CRUD methods:

@RestController
@RequestMapping("/v1/annotated/books")
public class AnnotationController {
    private final BookService bookService;

    public AnnotationController(BookService bookService) {
        this.bookService = bookService;
    }

    @GetMapping
    public Flux<Book> getAllBooks() {
        return bookService.getAllBooks();
    }
    @GetMapping("/{id}")
    public Mono<Book> getBookById(@PathVariable String id) {
        return bookService.getBookById(id);
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Mono<Book> createBook(@RequestBody Book book) {
        return bookService.createBook(book);
    }

    @PutMapping
    public Mono<Book> updateBook(@RequestBody Book book) {
        return bookService.updateBook(book);
    }

    @DeleteMapping("/{id}")
    public Mono<Void> deleteBookById(@PathVariable String id) {
        return bookService.deleteBookById(id);
    }

    @DeleteMapping
    public Mono<Void> deleteAllBooks() {
        return bookService.deleteAllBooks();
    }
}

2) Using functional endpoints

These endpoints are created using the functional programming style. This means it heavily makes use of lambda expressions. Also, this method is more lightweight than the annotations method, as it uses the same Reactive Core Foundation. It uses a functional programming model in which functions are used to route and handle requests.

@Configuration
@EnableWebFlux
public class FunctionalController {
    private static String BASE_URL = "/v1/functional/books";
    private final BookService bookService;

    public FunctionalController(BookService bookService) {
        this.bookService = bookService;
    }

    @Bean
    public RouterFunction<ServerResponse> getAllBooks() {
        return RouterFunctions.route()
                .GET(BASE_URL, request -> ServerResponse.ok().body(bookService.getAllBooks(), Book.class)).build();
    }

    @Bean
    public RouterFunction<ServerResponse> getBookById() {
        return RouterFunctions.route()
                .GET(BASE_URL.concat("/{id}"), request -> {
                    String id = request.pathVariable("id");
                    return ServerResponse.ok().body(bookService.getBookById(id), Book.class);
                }).build();
    }

    @Bean
    public RouterFunction<ServerResponse> createBook() {
        return RouterFunctions.route()
                .POST(BASE_URL, request -> request.bodyToMono(Book.class)
                        .flatMap(bookService::createBook)
                        .flatMap(book -> ServerResponse.status(HttpStatus.CREATED)
                                .body(book, Book.class))).build();
    }

    @Bean
    public RouterFunction<ServerResponse> updateBook() {
        return RouterFunctions.route()
                .PUT(BASE_URL, request -> request.bodyToMono(Book.class)
                        .flatMap(bookService::updateBook)
                        .flatMap(book -> ServerResponse.ok()
                                .body(book, Book.class))).build();
    }

    @Bean
    public RouterFunction<ServerResponse> deleteBookById() {
        return RouterFunctions.route()
                .DELETE(BASE_URL.concat("/{id}"), request -> {
                    String id = request.pathVariable("id");
                    return ServerResponse.ok().body(bookService.deleteBookById(id), Void.class);
                }).build();
    }

    @Bean
    public RouterFunction<ServerResponse> deleteAllBooks() {
        return RouterFunctions.route()
                .DELETE(BASE_URL, request -> ServerResponse.ok()
                        .body(bookService.deleteAllBooks(), Void.class)).build();
    }
}

The RouterFunction Class represents the main entry point of a request when we use the functional style. It takes a request and returns a response wrapped in the ServerResponse  Class. It can be seen as the equivalent of the request mappings and their associated methods when using the annotated style. The RouterFunctions Class is one that is used to build RouterFunctions using the builder pattern. We use it to give the structure of the request and how the request will be used to form the response. You can refer to the part of the Webflux Documentation on RouterFunctions to have more in-depth knowledge of how it works.

Testing

You can test the application by adding a Bootstrap class that will load some data into the Database and later use Postman or any similar tool to test the different endpoints.

@Component
@Slf4j
public class Bootstrap implements CommandLineRunner {
    private final BookRepository bookRepository;

    public Bootstrap(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    @Override
    public void run(String... args) throws Exception {
        Book book1 = new Book("Docker In Action", "Florian Lowe");
        Book book2 = new Book("Java Best Practices", "Sergey");
        Book book3 = new Book("Reactive Programming in C#", "Satoshi Nakamoto");

        bookRepository.saveAll(Arrays.asList(book1, book2, book3))
                .subscribe(book -> log.info("Save Books with Name: {}", book.getName()));
    }
}

Conclusion

This marks the end of this tutorial. You can now use Spring Webflux to work on your different projects. Hope this has been helpful to you. See you in the following tutorial.

Leave a Reply

Your email address will not be published. Required fields are marked *