Leveraging Spring Reactive, Functional Endpoints, Docker, and MongoDB

saad elattar

Elattar Saad

Thu Feb 29 2024 | 4 min read

Blocking is a feature of classic servlet-based web frameworks like Spring MVC. Introduced in Spring 5, Spring WebFlux is a reactive framework that operates on servers like Netty and is completely non-blocking.

Two programming paradigms are supported by Spring WebFlux. Annotations (Aspect Oriented Programming) and WebFlux.fn (Functional Programming).

"Spring WebFlux includes WebFlux.fn, a lightweight functional programming model in which functions are used to route and handle requests and contracts are designed for immutability. It is an alternative to the annotation-based programming model but otherwise runs on the same Reactive Core foundation." Spring | Functional Endpoints

Project Description

As the title describe, this is a simple Songs API build using Spring, Docker and MongoDB, the endpoints are Functional Endpoints and will have the traditional ControllerAdvice as Exception handler.

Project Dependencies

Talking XML these are the project dependencies:

pom.xml
<dependencies>
<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>org.springframework.boot</groupId>
<artifactId>spring-boot-docker-compose</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

Coding time!

First, let's setup the docker compose file /compose.yaml of the project (it should generated by spring via the docker support starter).

compose.yaml
services:
mongodb:
image: 'mongo:7.0.5'
environment:
- 'MONGO_INITDB_DATABASE=songsDB'
- 'MONGO_INITDB_ROOT_PASSWORD=passw0rd'
- 'MONGO_INITDB_ROOT_USERNAME=root'
ports:
- '27017'

With that set, let's create the Song class:

Song.java

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

import java.util.UUID;

@Document
@Getter
@Setter
@AllArgsConstructor
@Builder
public class Song {
@Id
private UUID id;
private String title;
private String artist;
}

The SongRepository interface will be referring to the Song class in its DB ops:

SongRepository.java
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;

import java.util.UUID;

@Repository
public interface SongRepository extends ReactiveCrudRepository<Song, UUID> {

Flux<Song> findAllByArtist(final String artist);

}

Song Functional Endpoint and Handler

Now, it's time for the Song Router, it will be responsible for router the incoming requests for the /songs ressource:

SongRouterConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;

import static org.springframework.web.reactive.function.server.RouterFunctions.route;

@Configuration
public class SongRouterConfig {

private final SongHandler handler;

public SongRouterConfig(SongHandler handler) {
this.handler = handler;
}

@Bean
public RouterFunction<ServerResponse> router() {
return route().path("/songs", builder -> builder
.GET("/artist", handler::findAllByArtist)
.GET(handler::findAll) // Get endpoints' order is important
.POST("/new", handler::create)
.DELETE("/{id}", handler::delete)
).build();
}
}

As you noticed the request are redirected to the SongHandler for a certain logic to be performed.

The SongsHandler will act as Service as well, will perform a business logic and communicate with the SongRepository for operations with the database.

SongHandler.java
import io.daasrattale.webfluxmongofunctionalendpoints.song.exceptions.InvalidParamException;
import io.daasrattale.webfluxmongofunctionalendpoints.song.exceptions.InvalidUUIDException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;

import java.util.Optional;
import java.util.UUID;

@Service
public class SongHandler {

private final SongRepository repository;

public SongHandler(SongRepository repository) {
this.repository = repository;
}

public Mono<ServerResponse> findAll(final ServerRequest request) {
return ServerResponse
.ok()
.body(repository.findAll(), Song.class);
}

public Mono<ServerResponse> findAllByArtist(final ServerRequest request) {
return Mono.just(request.queryParam("artist"))
.switchIfEmpty(Mono.error(new InvalidParamException("artist")))
.map(Optional::get)
.map(repository::findAllByArtist)
.flatMap(songFlux -> ServerResponse
.ok()
.body(songFlux, Song.class));
}

public Mono<ServerResponse> create(final ServerRequest request) {
return request.bodyToMono(Song.class)
.switchIfEmpty(Mono.error(new RuntimeException("Song body not found"))) // you can use that or create a custom exception (recommended)
.doOnNext(song -> song.setId(UUID.randomUUID()))
.flatMap(song -> ServerResponse
.status(HttpStatus.CREATED)
.body(repository.save(song), Song.class)
);
}

public Mono<ServerResponse> delete(final ServerRequest request) {
return Mono.just(request.pathVariable("id"))
.map(UUID::fromString)
.doOnError(throwable -> {
throw new InvalidUUIDException(throwable);
})
.flatMap(songId -> ServerResponse
.ok()
.body(repository.deleteById(songId), Void.class)
);
}
}

Exception Handling

As previously states, will be using the same old ControllerAdvice as Exception handler with two custom Exceptions as the following:

Custom Exceptions

InvalidParamException.java
import lombok.Getter;

@Getter
public class InvalidParamException extends RuntimeException {

private final String paramName;

public InvalidParamException(final String paramName) {
this.paramName = paramName;
}
}
InvalidUUIDException.java
import lombok.Getter;

@Getter
public class InvalidUUIDException extends RuntimeException {

private final Throwable cause;

public InvalidUUIDException(final Throwable cause) {
this.cause = cause;
}
}

Custom Exception Handler

SongExceptionHandler.java
import io.daasrattale.webfluxmongofunctionalendpoints.song.exceptions.InvalidUUIDException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.util.Map;

@ControllerAdvice
@Slf4j
public class SongExceptionHandler {


@ExceptionHandler(InvalidUUIDException.class)
public ResponseEntity<Map<String, ?>> handle(final InvalidUUIDException exception) {
return ResponseEntity
.badRequest()
.body(
Map.of(
"status", 400,
"message", "Invalid UUID",
"details", exception.getCause().getMessage()
)
);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, ?>> handle(final Exception exception) {
log.error("Unhandled Error, message: {}", exception.getMessage());
return ResponseEntity
.internalServerError()
.body(
Map.of(
"status", 500,
"message", "Unknown Error",
"details", exception.getMessage()
)
);
}
}

With all that been set, let's make use of our endpoint using Postman:

Spring Webflux Functional Endpoints

Spring Webflux Functional Endpoints

Spring Webflux Functional Endpoints

Sorry not a big fan of Madonna tbh :|

Spring Webflux Functional Endpoints

Spring Webflux Functional Endpoints

Finally,

With that said, our functional songs endpoint will be good to go for further improvements and new features. This is simple, in real industrial projects, I can assure you it can get complicated with more layers, for "getting started" purposes I avoided the use of advanced concepts such as validation, DTO, etc.

You can find the full source code here

Read next

Saad Elattar © 2024
|