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.