Spring WebFlux Functional Endpoints CRUD REST API Example

Spring WebFlux supports two types of programming models: 
  • The traditional annotation-based model with @Controller@RestController@RequestMapping, and other annotations that you have been using in Spring MVC. 
  • A brand new Functional style model based on Java 8 lambdas for routing and handling requests.

In this tutorial, we will new functional-style programming model to build reactive CRUD REST APIs using Spring Boot 3, Spring WebFlux, MongoDB, and IntelliJ IDEA.

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.

1. Create Spring Boot Application

Let's create a Spring boot application using Spring Intializr.

Refer to the below screenshot to enter the details while generating the Spring boot project using Spring Intializr:
Note that we are using Spring WebFlux, MongoDB Reactive, and Lombok libraries.

Here is the complete pom.xml file for your reference:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.0.1</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>net.javaguides</groupId>
	<artifactId>springboot-webflux-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>springboot-webflux-demo</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>17</java.version>
	</properties>
	<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>
			<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>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

2. Project Structure

Refer to the below screenshot to create the packing or project structure for the application:

3. Configure MongoDB

You can configure MongoDB by simply adding the following property to the application.properties file:
spring.data.mongodb.uri=mongodb://localhost:27017/ems
Spring Boot will read this configuration on startup and automatically configure the data source.

4. Create Domain Class

Let's create a Post MongoDB document and add the following content to it:
import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;

import java.time.LocalDateTime;
import java.util.List;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Document
public class Post {
    @Id
    private String id;
    private String title;
    private String description;
    private String body;
    @Field(name = "created_on")
    private LocalDateTime createdOn;
    @Field(name = "updated_on")
    private LocalDateTime updatedOn;
}

5. Creating Repository - EmployeeRepository

Next, we’re going to create the data access layer which will be used to access the MongoDB database.

Let's create a PostReactiveRepository interface and add the following content to it:

package net.javaguides.springbootwebfluxdemo.repository;

import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Mono;

import net.javaguides.springbootwebfluxdemo.entity.Post;
public interface PostReactiveRepository extends ReactiveCrudRepository<Post, String> {
    Mono<Boolean> existsByTitle(String title);
}
The PostReactiveRepository interface extends from ReactiveMongoRepository which exposes various CRUD methods on the Document. Spring Boot automatically plugs in an implementation of this interface called SimpleReactiveMongoRepository at runtime.

So you get all the CRUD methods on the Document readily available to you without needing to write any code.

6. Create PostMapper - Map Entity to Dto and Vice Versa

Let's create PostMapper class to map an entity to Dto and vice versa:
package net.javaguides.springbootwebfluxdemo.mapper;

import org.springframework.stereotype.Component;
import net.javaguides.springbootwebfluxdemo.dto.PostDto;
import net.javaguides.springbootwebfluxdemo.entity.Post;

@Component
public class PostMapper {

    public Post mapToPost(PostDto postInput) {
        return Post.builder()
                .title(postInput.getTitle())
                .description(postInput.getDescription())
                .body(postInput.getBody())
                .build();
    }

    public PostDto mapToPostDto(Post post) {
        return PostDto.builder()
                .id(post.getId())
                .title(post.getTitle())
                .description(post.getDescription())
                .body(post.getBody())
                .build();
    }
}

7. Create a Service Layer

PostService Interface

Let's create a PostService interface and add below CRUD methods to it:
package net.javaguides.springbootwebfluxdemo.service;

import net.javaguides.springbootwebfluxdemo.dto.PostDto;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

public interface PostService {
    Mono<PostDto> save(PostDto postDto);

    Flux<PostDto> findAllPosts();

    Mono<PostDto> update(PostDto postDto, String id);

    Mono<Void> delete(String id);
}

PostServiceImpl class

Let's create PostServiceImpl class that implements the PostService interface and its methods:
package net.javaguides.springbootwebfluxdemo.service.impl;

import lombok.AllArgsConstructor;
import net.javaguides.springbootwebfluxdemo.service.PostService;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import net.javaguides.springbootwebfluxdemo.repository.PostReactiveRepository;
import net.javaguides.springbootwebfluxdemo.mapper.PostMapper;
import net.javaguides.springbootwebfluxdemo.dto.PostDto;
import net.javaguides.springbootwebfluxdemo.entity.Post;

import java.time.LocalDateTime;

@Service
@AllArgsConstructor
public class PostServiceImpl implements PostService {

    private PostReactiveRepository postReactiveRepository;
    private PostMapper postMapper;

    @Override
    public Mono<PostDto> save(PostDto postDto) {
        Post post = postMapper.mapToPost(postDto);
        post.setCreatedOn(LocalDateTime.now());
        post.setUpdatedOn(LocalDateTime.now());
        return postReactiveRepository.save(post).map(p -> {
                    postDto.setId(p.getId());
                    return postDto;
                }
        );
    }

    @Override
    public Flux<PostDto> findAllPosts() {
        return postReactiveRepository.findAll()
                .map(postMapper::mapToPostDto)
                .switchIfEmpty(Flux.empty());
    }

    public Boolean postExistsWithTitle(String title) {
        return postReactiveRepository.existsByTitle(title).block();
    }

    @Override
    public Mono<PostDto> update(PostDto postDto, String id) {
        return postReactiveRepository.findById(id)
                .flatMap(savedPost -> {
                    Post post = postMapper.mapToPost(postDto);
                    post.setId(savedPost.getId());
                    return postReactiveRepository.save(post);
                })
                .map(postMapper::mapToPostDto);
    }

    @Override
    public Mono<Void> delete(String id) {
        return postReactiveRepository.deleteById(id);
    }
}

8. Create Controller Layer - Define Handler Function

Let's create a Handler function for CRUD operations.

In WebFlux.fn, an HTTP request is handled with a HandlerFunction: a function that takes ServerRequest and returns a delayed ServerResponse (i.e. Mono<ServerResponse>). HandlerFunction is the equivalent of the body of a @RequestMapping method in the annotation-based programming model.

Let's create PostHandler class and add the following content to it:
package net.javaguides.springbootwebfluxdemo.controller;

import lombok.RequiredArgsConstructor;
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 net.javaguides.springbootwebfluxdemo.service.impl.PostServiceImpl;
import net.javaguides.springbootwebfluxdemo.dto.PostDto;

@Component
@RequiredArgsConstructor
public class PostHandler {
    private final PostServiceImpl postService;

    public Mono<ServerResponse> listPosts(ServerRequest serverRequest) {
        Flux<PostDto> allPosts = postService.findAllPosts();
        Mono<ServerResponse> notFound = ServerResponse.notFound().build();

        return ServerResponse.ok()
                .contentType(MediaType.APPLICATION_JSON)
                .body(allPosts, PostDto.class)
                .switchIfEmpty(notFound);
    }

    public Mono<ServerResponse> savePost(ServerRequest serverRequest) {
        Mono<PostDto> postDtoMono = serverRequest.bodyToMono(PostDto.class);
        Mono<ServerResponse> notFound = ServerResponse.notFound().build();

        return postDtoMono.flatMap(postDto ->
                        ServerResponse
                                .status(HttpStatus.CREATED)
                                .contentType(MediaType.APPLICATION_JSON)
                                .body(postService.save(postDto), PostDto.class))
                .switchIfEmpty(notFound);
    }

    public Mono<ServerResponse> updatePost(ServerRequest serverRequest) {
        String id = serverRequest.pathVariable("id");
        Mono<PostDto> postDtoMono = serverRequest.bodyToMono(PostDto.class);
        Mono<ServerResponse> notFound = ServerResponse.notFound().build();

        return postDtoMono.flatMap(postDto ->
                        ServerResponse
                                .status(HttpStatus.OK)
                                .contentType(MediaType.APPLICATION_JSON)
                                .body(postService.update(postDto, id), PostDto.class))
                .switchIfEmpty(notFound);
    }

    public Mono<ServerResponse> deletePost(ServerRequest serverRequest) {
        String id = serverRequest.pathVariable("id");
        Mono<ServerResponse> notFound = ServerResponse.notFound().build();
        return ServerResponse
                .status(HttpStatus.NO_CONTENT)
                .build(postService.delete(id))
                .switchIfEmpty(notFound);
    }
}
The ServerRequest provides access to the HTTP method, URI, headers, and query parameters, while access to the body is provided through the body methods.

The ServerResponse provides access to the HTTP response and, since it is immutable, you can use a build method to create it. 

9. Define Route Function to Route the Requests

Incoming requests are routed to a handler function with a RouterFunction: a function that takes ServerRequest and returns a delayed HandlerFunction (i.e. Mono<HandlerFunction>). When the router function matches, a handler function is returned; otherwise returns an empty Mono.

RouterFunction is the equivalent of a @RequestMapping annotation, but with the major difference that router functions provide not just data, but also behavior.

package net.javaguides.springbootwebfluxdemo;

import net.javaguides.springbootwebfluxdemo.controller.PostHandler;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
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.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.nest;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

@SpringBootApplication
public class SpringbootWebfluxDemoApplication {

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

	@Bean
	RouterFunction<ServerResponse> routes(PostHandler postHandler) {
		return
				nest(path("/api/posts"),
						nest(accept(MediaType.APPLICATION_JSON),
								route(method(HttpMethod.GET), postHandler::listPosts)
										.andRoute(DELETE("/{id}"), postHandler::deletePost)
										.andRoute(POST("/"), postHandler::savePost)
										.andRoute(PUT("/{id}"), postHandler::updatePost)));

	}
}

10. Testing Reactive CRUD REST APIs using WebClientTest Class

Let's write the Integration test cases to test functional endpoints CRUD REST APIs using the WebTestClient class.

We are using the below WebTestClient class method to prepare CRUD REST API requests:
post() - Prepare an HTTP POST request.
delete() - Prepare an HTTP DELETE request. 
get() - Prepare an HTTP GET request. 
put() - Prepare an HTTP PUT request.

Here is the complete code for testing Spring WebFlux Reactive CRUD Rest APIs using WebTestClient:

package net.javaguides.springbootwebfluxdemo;

import net.javaguides.springbootwebfluxdemo.dto.EmployeeDto;
import net.javaguides.springbootwebfluxdemo.dto.PostDto;
import net.javaguides.springbootwebfluxdemo.entity.Post;
import net.javaguides.springbootwebfluxdemo.service.EmployeeService;
import net.javaguides.springbootwebfluxdemo.service.PostService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;

import java.util.Collections;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostHandlerTests {

    @Autowired
    private WebTestClient webTestClient;

    //@MockBean
    @Autowired
    private PostService postService;

    @Test
    public void testCreatePost() throws Exception {

        PostDto post = new PostDto();
        post.setTitle("Blog Post 1");
        post.setDescription("Blog Post 1 Description");
        post.setBody("Blog Post 1 Body");

        // when - action or behaviour that we are going test
        webTestClient.post().uri("/api/posts/")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .body(Mono.just(post), PostDto.class)
                .exchange()
                .expectStatus().isCreated()
                .expectBody()
                .consumeWith(System.out::println)
                .jsonPath("$.title").isEqualTo(post.getTitle())
                .jsonPath("$.description").isEqualTo(post.getDescription())
                .jsonPath("$.body").isEqualTo(post.getBody());
    }

    @Test
    public void testGetAllPosts() {
        webTestClient.get().uri("/api/posts")
                .accept(MediaType.APPLICATION_JSON)
                .exchange()
                .expectStatus().isOk()
                .expectHeader().contentType(MediaType.APPLICATION_JSON)
                .expectBodyList(PostDto.class)
                .consumeWith(System.out::println);
    }

    @Test
    public void testUpdateEmployee() throws Exception {

        PostDto post = new PostDto();
        post.setTitle("Blog Post 1");
        post.setDescription("Blog Post 1 Description");
        post.setBody("Blog Post 1 Body");

        PostDto updatedPost = new PostDto();
        updatedPost.setTitle("Blog Post 1 updated");
        updatedPost.setDescription("Blog Post 1 Description updated");
        updatedPost.setBody("Blog Post 1 Body updated");

        PostDto savedPost = postService.save(post).block();

        webTestClient.put()
                .uri("api/posts/{id}", Collections.singletonMap("id", savedPost.getId()))
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .body(Mono.just(updatedPost), PostDto.class)
                .exchange()
                .expectStatus().isOk()
                .expectHeader().contentType(MediaType.APPLICATION_JSON)
                .expectBody()
                .consumeWith(System.out::println)
                .jsonPath("$.title").isEqualTo(updatedPost.getTitle())
                .jsonPath("$.description").isEqualTo(updatedPost.getDescription())
                .jsonPath("$.body").isEqualTo(updatedPost.getBody());
    }

    @Test
    public void testDeletePost() {
        PostDto post = new PostDto();
        post.setTitle("Blog Post 2");
        post.setDescription("Blog Post 2 Description");
        post.setBody("Blog Post 2 Body");

        PostDto savedPost = postService.save(post).block();

        webTestClient.delete()
                .uri("/api/posts/{id}", Collections.singletonMap("id",  savedPost.getId()))
                .exchange()
                .expectStatus().isNoContent()
                .expectBody()
                .consumeWith(System.out::println);
    }
}

Output:

Here is the output of all the JUnit test cases:

Conclusion


In this tutorial, we have seen how to use a functional-style programming model to build reactive CRUD REST APIs.

Check out all Spring boot tutorials at https://www.javaguides.net/p/spring-boot-tutorial.html

Comments