Just a simple Songs API using Spring Reactive with Functional Endpoints, Docker and MongoDB
Tags: spring, java, docker, mongodb
Published: 2024-02-29 00:55:13

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](https://docs.spring.io/spring-framework/reference/web/webflux-functional.html)

## 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](https://spring.io/blog/2023/06/21/docker-compose-support-in-spring-boot-3-1).
- Lombok (Optional).

Talking XML these are the project dependencies:

```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).

```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:

```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:

```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:

```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.

> Note: If you 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.

```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)
                );
    }
}
```

> Note: 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
```java
import lombok.Getter;

@Getter
public class InvalidParamException extends RuntimeException {

    private final String paramName;

    public InvalidParamException(final String paramName) {
        this.paramName = paramName;
    }
}
```

```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
```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:

- Creating a new Song

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0f7tvnuyajf7x9asbuyz.png)

- Getting songs by artist:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/939xjmtqvw2uhh309q30.png)

- Getting all songs:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/envukk15y5l77rwwubey.png)

- Deleting a song:

Sorry not a big fan of Madonna tbh :|


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wu4g85olizwlc2897gnq.png)

- Checking the result of the delete op:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r1oxqh4z73h0xevyclel.png)


## 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 things 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](https://github.com/daasrattale/webflux-functional-endpoints)

Also find more content on my personal [personal website](https://saadelattar.me).