Validate functional endpoints in Spring

November 21, 2017 spring, REST

Spring WebFlux offers two reactive programming models to map and processes web requests. The "Annotated Controllers" model is consistent with Spring MVC, which allows the use of the "@Valid" annotation in contrast to the "Function Endpoints" model, which is more lightweight, lambda-based, and comparable to the approach of the "Play Framework."
In this blog post, I'll demonstrate how to add validation to "Functional Endpoints."

To start with, I've created a small rest service using Spring-Boot and MongoDB.

package foo.bar.springfunctionalwebvalidation;

import org.springframework.boot.autoconfigure.SpringBootApplication;  
import org.springframework.boot.builder.SpringApplicationBuilder;

@SpringBootApplication
public class App {

    public static void main(String[] args) {
        new SpringApplicationBuilder()
                .sources(App.class)
                .build()
                .run(args);
    }
}
package foo.bar.springfunctionalwebvalidation.repo;

import org.springframework.data.annotation.Id;  
import org.springframework.data.mongodb.core.mapping.Document;

@Document
public class Book {  
    @Id
    private String ref;
    private String title;
    private String author;

    ...
}
package foo.bar.springfunctionalwebvalidation.repo;

import org.springframework.data.mongodb.repository.ReactiveMongoRepository;

public interface BookRepo extends ReactiveMongoRepository<Book, String> {  
}
package foo.bar.springfunctionalwebvalidation.controller;

public class AddBook {  
    private String title;
    private String author;

    public String getTitle() {
        return title;
    }

    public String getAuthor() {
        return author;
    }

    public AddBook setTitle(String title) {
        this.title = title;
        return this;
    }

    public AddBook setAuthor(String author) {
        this.author = author;
        return this;
    }
}
package foo.bar.springfunctionalwebvalidation.controller;

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.RequestPredicates.GET;  
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;  
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

@Configuration
public class BookRoute {

    private static final String ENDPOINT = "/books";

    @Bean
    public RouterFunction<ServerResponse> bookRouter(final BookHandler handler) {
        return route(
                GET(ENDPOINT), handler::getBooks
        ).and(route(
                POST(ENDPOINT), handler::addBook)
        );
    }
}
package foo.bar.springfunctionalwebvalidation.controller;

import foo.bar.springfunctionalwebvalidation.repo.Book;  
import foo.bar.springfunctionalwebvalidation.repo.BookRepo;  
import org.springframework.stereotype.Component;  
import org.springframework.web.reactive.function.server.ServerRequest;  
import org.springframework.web.reactive.function.server.ServerResponse;  
import reactor.core.publisher.Mono;


import java.net.URI;

import static org.springframework.web.reactive.function.BodyInserters.fromPublisher;  
import static org.springframework.web.reactive.function.server.ServerResponse.ok;

@Component
public class BookHandler {

    private final BookRepo bookRepo;

    public BookHandler(BookRepo bookRepo) {
        this.bookRepo = bookRepo;
    }

    public Mono<ServerResponse> getBooks(ServerRequest request) {

        return ok()
                .body(fromPublisher(bookRepo.findAll(), Book.class));
    }

    public Mono<ServerResponse> addBook(ServerRequest request) {

        Mono<Book> bookMono = request
                .bodyToMono(AddBook.class)
                .map(addBook -> Book.of(addBook.getTitle(), addBook.getAuthor()));

        return bookRepo.insert(bookMono)
                .map(Book::getRef)
                .map(ref -> URI.create("http://localhost:8080/books/" + ref))
                .map(ServerResponse::created)
                .flatMap(ServerResponse.HeadersBuilder::build)
                .next();
    }

}

Now let's add validation to the "AddBook" command. In Java, we traditionally use the "JSR-303: Bean Validation" specification. For this example, let's add a "@NotEmpty" constraint.

import javax.validation.constraints.NotEmpty;

public class AddBook {  
    @NotEmpty
    private String title;
    @NotEmpty
    private String author;

We usually only need to add the "@Valid" annotation to our controller method, and our object gets validated. However, with "Function Endpoints," you don't have this option; the application is responsible for handling the request and generating an appropriated response.

So how do we validate an object without the "@Valid" annotation?
Well, it's quite easy; we only need to inject an instance "javax.validation.Validator" and pass the object to the "validate" method. Of course, "Spring Boot" adds a default validator to the application context, so we get it for free!

You mean repeating the boilerplate logic yourself?
No, no, please don't do that! Write a simple reusable function!
The function below takes a "ServerRequest," the class of the body, and a block of code as a parameter. When the validation passes, it will apply the body to the intersection of code. When the validation fails, it will stop processing the request and return an adequate response.

package foo.bar.springfunctionalwebvalidation.controller;

import org.springframework.stereotype.Component;  
import org.springframework.web.reactive.function.server.ServerRequest;  
import org.springframework.web.reactive.function.server.ServerResponse;  
import reactor.core.publisher.Mono;

import javax.validation.Validator;  
import java.util.function.Function;

@Component
public class RequestHandler {

    private final Validator validator;

    public RequestHandler(Validator validator) {
        this.validator = validator;
    }

    public <BODY> Mono<ServerResponse> requireValidBody(
            Function<Mono<BODY>, Mono<ServerResponse>> block,
            ServerRequest request, Class<BODY> bodyClass) {

        return request
                .bodyToMono(bodyClass)
                .flatMap(
                        body -> validator.validate(body).isEmpty()
                                ? block.apply(Mono.just(body))
                                : ServerResponse.unprocessableEntity().build()
                );
    }
}

The only thing left is to inject the RequestHandler, rewrite our code within a lambda, and pass it to the "requireValidBody" method.

    ...
    public BookHandler(BookRepo bookRepo, RequestHandler requestHandler) {
        this.bookRepo = bookRepo;
        this.requestHandler = requestHandler;
    }
    ...
    public Mono<ServerResponse> addBook(ServerRequest request) {

        return requestHandler.requireValidBody(body -> {
            Mono<Book> bookMono = body.map(addBook -> Book.of(addBook.getTitle(), 
                addBook.getAuthor()));

            return bookRepo.insert(bookMono)
                    .map(Book::getRef)
                    .map(ref -> URI.create("http://localhost:8080/books/" + ref))
                    .map(ServerResponse::created)
                    .flatMap(ServerResponse.HeadersBuilder::build)
                    .next();
        }, request, AddBook.class);
    }

To me, this approach looks natural and clean. I've been using a similar aim when validating endpoints coded in "Play." You don't have to repeat the tiresome validation stuff, yet the validation happens explicitly. Happy coding!

All code is available on github.

Jeroen Bellen
Alken
Dissident blogger!