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)