Creating a Reactive REST API with Spring Boot

Introduction

Building reactive applications allows for greater scalability and performance, especially in environments with high concurrency requirements. This guide demonstrates how to build a REST API using the Spring Boot Reactive Stack with WebFlux, allowing for non-blocking, asynchronous handling of API requests. We’ll also explore configuring serialization, setting up a data model, and building services to manage data flow in a reactive manner.

Prerequisites

Ensure you have Java 17, Maven, and HTTPie installed. HTTPie will help us interact with the API endpoints and validate their responses.

Step 1: Install HTTPie

HTTPie is a powerful command-line HTTP client that’s straightforward to use for testing REST APIs.

sudo apt install httpie

Step 2: Generate the Project with Spring Initializr

We’ll start by generating a new project using Spring Initializr, setting up a Java 17 project with the com.outadi.siavash package, artifact ID reactive-rest-api, and dependencies for WebFlux and an H2 embedded database.

http https://start.spring.io/starter.tgz groupId==com.outadi.siavash \
                                         artifactId==reactive-rest-api \
                                         dependencies==webflux,h2 \
                                         javaVersion==17 \
                                         type==maven-project \
                                         baseDir==reactive-rest-api | tar -xzvf -

Next, open the project in Visual Studio Code:

cd reactive-rest-api && code .

Step 3: Configure API Settings

Enable WebFlux and configure JSON serialization and deserialization using Jackson in the API configuration file. This will allow the API to correctly handle Java 8 date/time formats and ignore unknown properties.

Create the configuration file:

code ./src/main/java/com/outadi/siavash/reactiverestapi/config/ApiConfig

Add the following code:

package com.outadi.siavash.reactiverestapi.config;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.config.EnableWebFlux;

@Configuration
@EnableWebFlux
public class ApiConfig {
    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

        return objectMapper;
    }

    @Bean
    public ObjectWriter objectWriter(ObjectMapper objectMapper) {
        return objectMapper.writerWithDefaultPrettyPrinter();
    }
}

Step 4: Create the Data Model

We’ll create a Book model to represent books in our application. This class includes attributes for book details and annotations for handling date formatting.

code ./src/main/java/com/outadi/siavash/reactiverestapi/model/Book.java

Add the following content:

package com.outadi.siavash.reactiverestapi.model;

import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.LocalDate;
import org.springframework.format.annotation.DateTimeFormat;

public class Book {
  private String Id;
  private String title;
  @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
  @JsonFormat(pattern = "yyyy-MM-dd")
  private LocalDate publishedDate;
  private int pages;

  public Book() {
  }

  public Book(String id, String title, LocalDate publishedDate, int pages) {
    Id = id;
    this.title = title;
    this.publishedDate = publishedDate;
    this.pages = pages;
  }

  public String getId() {
    return Id;
  }

  public String getTitle() {
    return title;
  }

  public void setTitle(String title) {
    this.title = title;
  }

  public LocalDate getPublishedDate() {
    return publishedDate;
  }

  public void setPublishedDate(LocalDate publishedDate) {
    this.publishedDate = publishedDate;
  }

  public int getPages() {
    return pages;
  }

  public void setPages(int pages) {
    this.pages = pages;
  }
}

Step 5: Implement the Service Layer

The service layer will manage business logic. We’ll define a BookService interface and implement it in BookServiceImpl to handle data retrieval. For simplicity, this example uses mock data.

Create the service interface:

code ./src/main/java/com/outadi/siavash/reactiverestapi/service/BookService.java
package com.outadi.siavash.reactiverestapi.service;

import com.outadi.siavash.reactiverestapi.model.Book;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

public interface  BookService {
    Flux<Book> getBooks();

    Mono<Book> getBookById(String bookId);
}

Next the service implementation is added to the logic to perform the operations. In this case a mock data is generated for simplicity.

code ./src/main/java/com/outadi/siavash/reactiverestapi/service/BookServiceImpl.java

Add the following content:

package com.outadi.siavash.reactiverestapi.service;

import com.outadi.siavash.reactiverestapi.model.Book;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.IntStream;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Service
public class BookServiceImpl implements BookService {
  List<Book> BOOKS = generateBooks();

  @Override
  public Flux<Book> getBooks() {
    return Flux.fromIterable(BOOKS);
  }

  @Override
  public Mono<Book> getBookById(String bookId) {
    for (Book book : BOOKS) {
      if (book.getId().equals(bookId)) {
        return Mono.just(book);
      }
    }

    throw new RuntimeException("Book not found!");
  }

  private List<Book> generateBooks() {
    List<Book> books = new ArrayList<>();
    IntStream.range(0, 5).forEachOrdered(n -> {
      books.add(generateBook());
    });

    return books;
  }

  private Book generateBook() {
    String id = UUID.randomUUID().toString();
    int page = new Random().nextInt(100) + 100;
    Instant instance = Instant.ofEpochSecond(ThreadLocalRandom.current().nextInt());
    LocalDate publishedDate = LocalDate.ofInstant(instance, ZoneId.systemDefault());
    return new Book(id, "Book " + id, publishedDate, page);
  }
}

Step 6: Build the REST Controller

The controller layer exposes endpoints to retrieve all books or fetch a book by its ID.

code ./src/main/java/com/outadi/siavash/reactiverestapi/controller/BookResource.java
package com.outadi.siavash.reactiverestapi.controller;

import com.outadi.siavash.reactiverestapi.model.Book;
import com.outadi.siavash.reactiverestapi.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping(BookResource.API_V1_BOOK)
public class BookResource {
  static final String API_V1_BOOK = "/api/v1/book";

  @Autowired
  private BookService bookService;

  @GetMapping(path = "{bookId}", produces = MediaType.APPLICATION_JSON_VALUE)
  public Mono<Book> getBookById(@PathVariable String bookId) {
    return bookService.getBookById(bookId);
  }

  @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
  public Flux<Book> getBookById() {
    return bookService.getBooks();
  }
}

Step 7: Build and Run the Application

Use Maven to build the application:

mvn clean install

Then, start the server:

java -jar target/reactive-rest-api-0.0.1-SNAPSHOT.jar

Testing the API

Use HTTPie to test the API:

Get All Books

http http://localhost:8080/api/v1/book
HTTP/1.1 200 OK
Content-Type: application/json
transfer-encoding: chunked

[
    {
        "id": "a427df85-a50e-4c0b-967a-11784d64efe0",
        "pages": 159,
        "publishedDate": "1915-12-14",
        "title": "Book a427df85-a50e-4c0b-967a-11784d64efe0"
    },
    {
        "id": "ea371e0d-ed54-405b-bbbd-aab11b1560e3",
        "pages": 170,
        "publishedDate": "1903-06-13",
        "title": "Book ea371e0d-ed54-405b-bbbd-aab11b1560e3"
    },
    {
        "id": "a42c0e43-08b6-4593-b926-79ca87975f87",
        "pages": 107,
        "publishedDate": "1945-01-07",
        "title": "Book a42c0e43-08b6-4593-b926-79ca87975f87"
    },
    {
        "id": "ab50aab8-d833-4b57-b789-a8fed5087de9",
        "pages": 134,
        "publishedDate": "1933-08-25",
        "title": "Book ab50aab8-d833-4b57-b789-a8fed5087de9"
    },
    {
        "id": "baa17a26-fb2e-41d7-a660-5de05c1ad7f7",
        "pages": 162,
        "publishedDate": "1958-09-12",
        "title": "Book baa17a26-fb2e-41d7-a660-5de05c1ad7f7"
    }
]

Get a Single Book

http http://localhost:8080/api/v1/book/baa17a26-fb2e-41d7-a660-5de05c1ad7f7
HTTP/1.1 200 OK
Content-Length: 138
Content-Type: application/json

{
    "id": "baa17a26-fb2e-41d7-a660-5de05c1ad7f7",
    "pages": 162,
    "publishedDate": "1958-09-12",
    "title": "Book baa17a26-fb2e-41d7-a660-5de05c1ad7f7"
}

Source Code

The complete source code is available on GitHub.