DEV Community

Dev Cookies
Dev Cookies

Posted on

Building Reactive REST APIs with Spring WebFlux Functional Endpoints

Here's a production-grade full project code snippet using Spring Boot WebFlux with functional-style routing. This includes:

  • Clean folder structure
  • DTOs, validation
  • Global error handling
  • Reactive service layer
  • Functional handlers & routers
  • MongoDB as persistence layer

πŸ“ Project Structure (Maven)

src/
└── main/
    └── java/com/example/userapi/
        β”œβ”€β”€ config/
        β”‚   └── RouterConfig.java
        β”œβ”€β”€ controller/
        β”‚   └── UserHandler.java
        β”œβ”€β”€ dto/
        β”‚   └── UserDto.java
        β”œβ”€β”€ exception/
        β”‚   β”œβ”€β”€ GlobalExceptionHandler.java
        β”‚   └── UserNotFoundException.java
        β”œβ”€β”€ model/
        β”‚   └── User.java
        β”œβ”€β”€ repository/
        β”‚   └── UserRepository.java
        β”œβ”€β”€ service/
        β”‚   └── UserService.java
        └── UserApiApplication.java
Enter fullscreen mode Exit fullscreen mode

πŸ“¦ 1. pom.xml

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>

    <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-validation</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>
Enter fullscreen mode Exit fullscreen mode

🧩 2. model/User.java

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Document(collection = "users")
public class User {
    @Id
    private String id;

    private String name;

    private String email;

    private int age;
}
Enter fullscreen mode Exit fullscreen mode

πŸ“₯ 3. dto/UserDto.java

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserDto {

    @NotBlank(message = "Name cannot be blank")
    private String name;

    @Email(message = "Email must be valid")
    private String email;

    @Min(value = 1, message = "Age must be at least 1")
    private int age;
}
Enter fullscreen mode Exit fullscreen mode

πŸ“‚ 4. repository/UserRepository.java

public interface UserRepository extends ReactiveMongoRepository<User, String> {
    Flux<User> findByNameContainingIgnoreCase(String name);
}
Enter fullscreen mode Exit fullscreen mode

βš™ 5. service/UserService.java

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    public Mono<User> createUser(UserDto dto) {
        User user = User.builder()
                .name(dto.getName())
                .email(dto.getEmail())
                .age(dto.getAge())
                .build();
        return userRepository.save(user);
    }

    public Mono<User> getUser(String id) {
        return userRepository.findById(id)
                .switchIfEmpty(Mono.error(new UserNotFoundException(id)));
    }

    public Flux<User> getAllUsers() {
        return userRepository.findAll();
    }

    public Flux<User> searchUsers(String name) {
        return userRepository.findByNameContainingIgnoreCase(name);
    }

    public Mono<User> updateUser(String id, UserDto dto) {
        return userRepository.findById(id)
            .switchIfEmpty(Mono.error(new UserNotFoundException(id)))
            .flatMap(existing -> {
                existing.setName(dto.getName());
                existing.setEmail(dto.getEmail());
                existing.setAge(dto.getAge());
                return userRepository.save(existing);
            });
    }

    public Mono<Void> deleteUser(String id) {
        return userRepository.findById(id)
            .switchIfEmpty(Mono.error(new UserNotFoundException(id)))
            .flatMap(userRepository::delete);
    }
}
Enter fullscreen mode Exit fullscreen mode

🧠 6. handler/UserHandler.java

@Component
@RequiredArgsConstructor
public class UserHandler {

    private final UserService userService;
    private final Validator validator;

    public Mono<ServerResponse> createUser(ServerRequest request) {
        return request.bodyToMono(UserDto.class)
            .flatMap(dto -> validate(dto))
            .flatMap(userService::createUser)
            .flatMap(user -> ServerResponse.status(HttpStatus.CREATED).bodyValue(user));
    }

    public Mono<ServerResponse> getUser(ServerRequest request) {
        String id = request.pathVariable("id");
        return userService.getUser(id)
            .flatMap(user -> ServerResponse.ok().bodyValue(user));
    }

    public Mono<ServerResponse> getAll(ServerRequest request) {
        return ServerResponse.ok().body(userService.getAllUsers(), User.class);
    }

    public Mono<ServerResponse> search(ServerRequest request) {
        String name = request.queryParam("name").orElse("");
        return ServerResponse.ok().body(userService.searchUsers(name), User.class);
    }

    public Mono<ServerResponse> update(ServerRequest request) {
        String id = request.pathVariable("id");
        return request.bodyToMono(UserDto.class)
            .flatMap(dto -> validate(dto))
            .flatMap(dto -> userService.updateUser(id, dto))
            .flatMap(user -> ServerResponse.ok().bodyValue(user));
    }

    public Mono<ServerResponse> delete(ServerRequest request) {
        String id = request.pathVariable("id");
        return userService.deleteUser(id)
            .then(ServerResponse.noContent().build());
    }

    private Mono<UserDto> validate(UserDto dto) {
        Errors errors = new BeanPropertyBindingResult(dto, UserDto.class.getName());
        validator.validate(dto, errors);
        if (errors.hasErrors()) {
            return Mono.error(new ConstraintViolationException(errors.toString(), Set.of()));
        }
        return Mono.just(dto);
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”€ 7. config/RouterConfig.java

@Configuration
public class RouterConfig {

    @Bean
    public RouterFunction<ServerResponse> route(UserHandler handler) {
        return RouterFunctions.route()
            .GET("/users", handler::getAll)
            .GET("/users/{id}", handler::getUser)
            .GET("/users/search", handler::search)
            .POST("/users", handler::createUser)
            .PUT("/users/{id}", handler::update)
            .DELETE("/users/{id}", handler::delete)
            .build();
    }
}
Enter fullscreen mode Exit fullscreen mode

⚠️ 8. exception/UserNotFoundException.java

public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(String id) {
        super("User not found with ID: " + id);
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ›‘οΈ 9. exception/GlobalExceptionHandler.java

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public Mono<ResponseEntity<String>> handleNotFound(UserNotFoundException ex) {
        return Mono.just(ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage()));
    }

    @ExceptionHandler(Exception.class)
    public Mono<ResponseEntity<String>> handleAll(Exception ex) {
        return Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Internal error: " + ex.getMessage()));
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸš€ 10. UserApiApplication.java

@SpringBootApplication
public class UserApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserApiApplication.class, args);
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ› οΈ 11. application.yml

spring:
  data:
    mongodb:
      uri: mongodb://localhost:27017/userdb
  main:
    web-application-type: reactive
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ Test Example with curl

curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Dev", "email": "dev@example.com", "age": 28}'

curl http://localhost:8080/users
curl http://localhost:8080/users/123
Enter fullscreen mode Exit fullscreen mode

Top comments (0)