Spring Boot Functional Endpoints

Prateek
3 min readJun 16, 2021

--

In this tutorial, we’ll see how to make the use of Functional endpoints using Spring Boot and webflux

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.projectlombok</groupId>
<artifactId>lombok</artifactId>
</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>

Product.java

@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
@Document
public class Product {
@Id
private String id;
private String name;
private Double price;
}

ProductEvent.java

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class ProductEvent {
private Long eventId;
private String eventType;
}

ProductRepository.java

import com.wiredbraincoffee.productapifunctional.model.Product;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;

public interface ProductRepository extends ReactiveMongoRepository<Product, String> {
}

ProductHandler.java

import com.wiredbraincoffee.productapifunctional.model.Product;
import com.wiredbraincoffee.productapifunctional.model.ProductEvent;
import com.wiredbraincoffee.productapifunctional.repository.ProductRepository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
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.Flux;
import reactor.core.publisher.Mono;

import java.time.Duration;

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.BodyInserters.fromObject;

@Component
public class ProductHandler {
@Autowired
private ProductRepository repository;

public Mono<ServerResponse> getAllProducts(ServerRequest request) {
Flux<Product> products = repository.findAll();

return ServerResponse.ok()
.contentType(APPLICATION_JSON)
.body(products, Product.class);
}

public Mono<ServerResponse> getProduct(ServerRequest request) {
String id = request.pathVariable("id");

Mono<Product> productMono = this.repository.findById(id);
Mono<ServerResponse> notFound = ServerResponse.notFound().build();

return productMono
.flatMap(product ->
ServerResponse.ok()
.contentType(APPLICATION_JSON)
.body(fromObject(product)))
.switchIfEmpty(notFound);
}

public Mono<ServerResponse> saveProduct(ServerRequest request) {
Mono<Product> productMono = request.bodyToMono(Product.class);

return productMono.flatMap(product ->
ServerResponse.status(HttpStatus.CREATED)
.contentType(APPLICATION_JSON)
.body(repository.save(product), Product.class));
}

public Mono<ServerResponse> updateProduct(ServerRequest request) {
String id = request.pathVariable("id");
Mono<Product> existingProductMono = this.repository.findById(id);
Mono<Product> productMono = request.bodyToMono(Product.class);

Mono<ServerResponse> notFound = ServerResponse.notFound().build();

return productMono.zipWith(existingProductMono,
(product, existingProduct) ->
new Product(existingProduct.getId(), product.getName(), product.getPrice())
)
.flatMap(product ->
ServerResponse.ok()
.contentType(APPLICATION_JSON)
.body(repository.save(product), Product.class)
).switchIfEmpty(notFound);
}

public Mono<ServerResponse> deleteProduct(ServerRequest request) {
String id = request.pathVariable("id");

Mono<Product> productMono = this.repository.findById(id);
Mono<ServerResponse> notFound = ServerResponse.notFound().build();

return productMono
.flatMap(existingProduct ->
ServerResponse.ok()
.build(repository.delete(existingProduct))
)
.switchIfEmpty(notFound);
}

public Mono<ServerResponse> deleteAllProducts(ServerRequest request) {
return ServerResponse.ok()
.build(repository.deleteAll());
}

public Mono<ServerResponse> getProductEvents(ServerRequest request) {
Flux<ProductEvent> eventsFlux = Flux.interval(Duration.ofSeconds(1)).map(val ->
new ProductEvent(val, "Product Event")
);

return ServerResponse.ok()
.contentType(MediaType.TEXT_EVENT_STREAM)
.body(eventsFlux, ProductEvent.class);
}
}

MainApp.java

import com.wiredbraincoffee.productapifunctional.model.Product;
import com.wiredbraincoffee.productapifunctional.repository.ProductRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import reactor.core.publisher.Flux;

import com.wiredbraincoffee.productapifunctional.handler.ProductHandler;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.http.HttpMethod;

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.http.MediaType.TEXT_EVENT_STREAM;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.nest;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;


@SpringBootApplication
public class ProductApiFunctionalApplication {

public static void main(String[] args) {
SpringApplication.run(ProductApiFunctionalApplication.class, args);
}

@Bean
CommandLineRunner init(ReactiveMongoOperations operations, ProductRepository repository) {
return args -> {
Flux<Product> productFlux = Flux.just(
new Product(null, "Big Latte", 2.99),
new Product(null, "Big Decaf", 2.49),
new Product(null, "Green Tea", 1.99))
.flatMap(repository::save);

productFlux
.thenMany(repository.findAll())
.subscribe(System.out::println);

/*operations.collectionExists(Product.class)
.flatMap(exists -> exists ? operations.dropCollection(Product.class) : Mono.just(exists))
.thenMany(v -> operations.createCollection(Product.class))
.thenMany(productFlux)
.thenMany(repository.findAll())
.subscribe(System.out::println);*/
};
}

@Bean
RouterFunction<ServerResponse> routes(ProductHandler handler) {
// return route(GET("/products").and(accept(APPLICATION_JSON)), handler::getAllProducts)
// .andRoute(POST("/products").and(contentType(APPLICATION_JSON)), handler::saveProduct)
// .andRoute(DELETE("/products").and(accept(APPLICATION_JSON)), handler::deleteAllProducts)
// .andRoute(GET("/products/events").and(accept(TEXT_EVENT_STREAM)), handler::getProductEvents)
// .andRoute(GET("/products/{id}").and(accept(APPLICATION_JSON)), handler::getProduct)
// .andRoute(PUT("/products/{id}").and(contentType(APPLICATION_JSON)), handler::updateProduct)
// .andRoute(DELETE("/products/{id}").and(accept(APPLICATION_JSON)), handler::deleteProduct);

return nest(path("/products"),
nest(accept(APPLICATION_JSON).or(contentType(APPLICATION_JSON)).or(accept(TEXT_EVENT_STREAM)),
route(GET("/"), handler::getAllProducts)
.andRoute(method(HttpMethod.POST), handler::saveProduct)
.andRoute(DELETE("/"), handler::deleteAllProducts)
.andRoute(GET("/events"), handler::getProductEvents)
.andNest(path("/{id}"),
route(method(HttpMethod.GET), handler::getProduct)
.andRoute(method(HttpMethod.PUT), handler::updateProduct)
.andRoute(method(HttpMethod.DELETE), handler::deleteProduct)
)
)
);
}
}

How to test the app?

GET → http://localhost:8080/products

[
{
"id": "60c86d4b0506f78c840af472",
"name": "Big Latte",
"price": 2.99
},
{
"id": "60c86d4b0506f78c840af474",
"name": "Green Tea",
"price": 1.99
},
{
"id": "60c86d4b0506f78c840af473",
"name": "Big Decaf",
"price": 2.49
}
]

GET → http://localhost:8080/products/60c86d4b0506f78c840af474

{
"id": "60c86d4b0506f78c840af474",
"name": "Green Tea",
"price": 1.99
}

POST → http://localhost:8080/products

curl --location --request POST 'http://localhost:8080/products' \
--header 'Content-Type: application/json' \
--data-raw '{
"name" : "Chai",
"price" : 1.51
}'

Response —

{
"id": "60c86f7f0506f78c840af475",
"name": "Chai",
"price": 1.51
}

PUT → http://localhost:8080/products/60c86f7f0506f78c840af475

curl --location --request PUT 'http://localhost:8080/products/60c86f7f0506f78c840af475' \
--header 'Content-Type: application/json' \
--data-raw '{
"name" : "Chai22",
"price" : 1.51
}'

Response →

{
"id": "60c86f7f0506f78c840af475",
"name": "Chai22",
"price": 1.51
}

DELETE → http://localhost:8080/products/60c86f7f0506f78c840af475

Response → HTTP OK 200

DELETE → http://localhost:8080/products/

Response → HTTP OK 200

--

--

Prateek
Prateek

Written by Prateek

Java Developer and enthusiast

No responses yet