DEV Community

Cover image for Just a simple Songs API using Spring Reactive with Functional Endpoints, Docker and MongoDB
Elattar Saad
Elattar Saad

Posted on • Updated on

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>
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

The SongRepository interface will be referring 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);

}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

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)
                );
    }
}
Enter fullscreen mode Exit fullscreen mode

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

import lombok.Getter;

@Getter
public class InvalidParamException extends RuntimeException {

    private final String paramName;

    public InvalidParamException(final String paramName) {
        this.paramName = paramName;
    }
}
Enter fullscreen mode Exit fullscreen mode
import lombok.Getter;

@Getter
public class InvalidUUIDException extends RuntimeException {

    private final Throwable cause;

    public InvalidUUIDException(final Throwable cause) {
        this.cause = cause;
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
                        )
                );
    }
}
Enter fullscreen mode Exit fullscreen mode

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

  • Creating a new Song

Image description

  • Getting songs by artist:

Image description

  • Getting all songs:

Image description

  • Deleting a song:

Sorry not a big fan of Madonna tbh :|

Image description

  • Checking the result of the delete op:

Image description

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

Also find more content on my personal personal website.

Top comments (0)