Just A Simple Songs API Using Spring Reactive With Functional Endpoints, Docker And MongoDB
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
- Java Version 21
- Spring Boot version 3.3.0-SNAPSHOT with Spring Reactive Starter.
- Spring Docker Support.
- Lombok (Optional).
Talking XML these are the project dependencies:
<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).
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:
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 reffering to the Song class in its DB ops:
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:
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.
If you’re having trouble understanding the syntax, make sure to know more about Java functional interfaces, lambda and method references.
The SongsHandler will act as Service as well, will perform a business logic and communicate with the SongRepository for operations with the database.
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)
);
}
}
The SongHandler can be annotated with @Component, since it performs a business logic I see it better have the @Service annotation instead.
Exception Handling
As previously states, will be using the same old ControllerAdvice as Exception handler with two custom Exceptions as the following:
Custom Exceptions
import lombok.Getter;
@Getter
public class InvalidParamException extends RuntimeException {
private final String paramName;
public InvalidParamException(final String paramName) {
this.paramName = paramName;
}
}
import lombok.Getter;
@Getter
public class InvalidUUIDException extends RuntimeException {
private final Throwable cause;
public InvalidUUIDException(final Throwable cause) {
this.cause = cause;
}
}
Custom Exception Handler
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 use out endpoint using Postman.
- Creating a new song:
- Getting songs by artist:
- Getting all songs:
- Deleting a song:
Sorry not a big fan of Madonna tbh :|
- Checking the result of the delete op:
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 here
Also find more content on my personal website.