Avenue Code Snippets

Using Spring Webflux for REST APIs: Annotated and Functional Endpoints

Written by Diego Zanivan | 3/8/23 5:00 PM

Spring Webflux is the reactive stack of the Spring framework, and it enables the creation of non-blocking code using fewer hardware resources and threads. You can still create endpoints using annotations or the most recent functional method. In this blog, we will create endpoints using the functional style and the annotated style with Spring Webflux.

Prerequisites

Before we get started, you'll need to add the following Spring dependencies to the project:

  1. Spring Reactive Web - Webflux
  2. Lombok to create the boilerplate annotation
  3. Optional: MongoDB driver - I used MongoDB on this project
Annotated Endpoints

Annotated endpoints are intuitive, easier to learn, and frequently used by many companies since they are common in older Spring versions.

This is an example of how we can create an annotated controller:

@RestController
@RequestMapping("v1/activity")
public class ActivityV1Controller {

  @Autowired
  private ActivityService service;

  @PostMapping
  public Mono<Activity> save(@RequestBody Activity activity) {
      return service.save(activity);
  }

  @GetMapping("{id}")
  public Mono<Activity> getById(@PathVariable("id") String id) {
      return service.findById(id);
  }

  @GetMapping
  public Flux<Activity> getAll() {
      return service.findAll();
  }
}

The @RestController annotation is used to identify this class as an endpoint, and we have defined the URI path using @RequestMapping.

@PostMapping and @GetMapping are used to receive requests using POST verb and GET verb respectively. I've injected the ActivityService with @Autowired, and I'm using the Activity class in this controller, but don't worry about this for your own project.

This is basically an annotated endpoint. Pretty simple, isn't it?

Functional Endpoint

Now let's create an endpoint with the same behavior but using a functional endpoint.

First things first: functional endpoints are usually divided into a handler and a router. You don't absolutely need a handler, but without it your code will probably be a mess.

Handler

Let's start by creating the handler. It will be responsible for actions like saving or retrieving data.

@Component
public class ActivityHandler {

  @Autowired
  private ActivityService service;

  private ActivityConverter converter;

  public ActivityHandler() {
      converter = new ActivityConverter();
  }

  public Mono<ServerResponse> findAll(ServerRequest request) {
      return ok().contentType(MediaType.APPLICATION_JSON)
              .body(service.findAll()
                      .flatMap(x -> converter.convertToDto(x)), ActivityDto.class);
  }

  public Mono<ServerResponse> findById(ServerRequest request) {
      return ok().contentType(MediaType.APPLICATION_JSON)
              .body(service.findById(request.pathVariable("id"))
                      .flatMap(x -> converter.convertToDto(x)), ActivityDto.class);
  }

  public Mono<ServerResponse> save(ServerRequest request) {
      return ok().contentType(MediaType.APPLICATION_JSON)
              .body(request.bodyToMono(ActivityDto.class)
                      .flatMap(dto -> converter.convertToDocument(service, dto))
                      .flatMap(doc -> service.save(doc))
                      .doOnNext(doc -> converter.convertToDto(doc)), ActivityDto.class);
  }
}

I added the @Component annotation so that I can inject this class in my router.

I'm no longer using the Activity class, but I am using a DTO (data transfer object) representation instead. You don't need to worry about this, however, because it's totally optional, and I used it merely for the context of my own project.

The important thing to pay attention to is how we handle the incoming 'id' as a path parameter in the URL: localhost:8080/v2/activity/1, where 1 is the path parameter 'id'.

We can retrieve this id using the ServerRequest class and the method pathVariable(VARIABLE_NAME).

There's another difference we need to point out: When getting the request body, we used the ServerRequest again, but this time with the method bodyToMono(ClassToMap.class). Since we are receiving a DTO, we must convert it to the actual Activity.class. It would be easier if we had used the Activity.class instead.

Router

The router is responsible for mapping the URIs and executing actions when hitting them.

@Configuration(proxyBeanMethods = false)
public class ActivityRouter {

  @Bean
  public RouterFunction<ServerResponse> route(ActivityHandler activityHandler) {
      return RouterFunctions.route()
              .path("/v2/activity", builder -> builder
                      .GET("{id}", accept(MediaType.APPLICATION_JSON), activityHandler::findById)
                      .GET(accept(MediaType.APPLICATION_JSON), activityHandler::findAll)
                      .POST(accept(MediaType.APPLICATION_JSON), activityHandler::save)
                      .PUT(accept(MediaType.APPLICATION_JSON), activityHandler::save)
              )
              .build()
              .andRoute(GET("other"), req -> ServerResponse.ok()
                      .body(Mono.just("Other route"), String.class));
  }
}

There are different ways to create routes, but I personally like this one.

We have the @Configuration Spring annotation and the method annotated with @Bean that should be processed by the Spring container. This is necessary for Spring to properly map those URIs.

We have the route method that will receive our ActivityHandler as a parameter.

The method starts by defining the path '/v2/activity' for the URI since '/v1/activity' is already defined on the annotated controller.

The URI was grouped by the path; each verb (GET, POST, PUT) will be handled differently.

Finally, I've created the mock route 'other' just to show how we can add more routes and perform some actions without a handler.

Global Handler

Now we have our endpoints in place and everything is going fine. But we are the IT crew, and we know that things don't go well all the time. What if we throw some exception and we want a default return for this exception? This is where a global handler comes in handy!

We want to respond with a 400 Bad Request status whenever an exception is thrown.

Let's create a custom exception:

public class EntryNotFoundException extends RuntimeException {
  public EntryNotFoundException(String id) {
      super("No entry found for id: " + id);
  }
}

No big deal, right?

Let's now capture the error and map the return according to the exception.

There are many ways to do this, but an easy one that works for both functional and annotated endpoints is implementing the WebExceptionHandler interface.

@Component
@Order(-2)
public class RestWebExceptionHandler implements WebExceptionHandler {

  @Override
  public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
      if (ex instanceof EntryNotFoundException) {
          exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND);
      }

      if (ex instanceof InvalidEntryException) {
          exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
      }

      if (Strings.isNotBlank(ex.getMessage())) {
          byte[] bytes = ex.getMessage().getBytes(StandardCharsets.UTF_8);
          DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
          return exchange.getResponse().writeWith(Flux.just(buffer));
      }

      return Mono.error(ex);
  }

}

We need to annotate it with @Order(-2) to run our class before the default handler.

I'm not doing much, just changing the response status code according to the exception and merging the exception message with the response buffer before sending it back to the user.

Now you can throw the EntryNotFoundException at any point of your code and you'll notice that our RestWebExceptionHandler will come into action.

Conclusion

Which style did you prefer: annotated or functional? Let me know in the comments!

Please note that there are other ways to globally handle exceptions. I recommend researching them and choosing the best fit for your project.

All of this code is available on my Github.

 

Want to learn more about Spring WebFlux? Check out our quick start guide for building reactive REST APIs with Spring: