Functional HTTP endpoints with Spring MVC/WebFlux and Kotlin

Functional development approaches in Spring are becoming increasingly popular due to their flexibility and conciseness. In a new article from an expert communities Spring IO, Mikhail Polivakhalooks at how you can effectively define HTTP endpoints using Spring MVC/WebFlux using a functional programming style in Kotlin. A similar approach can be implemented in Java, although using Kotlin allows you to significantly simplify the code.


1. Introduction

In this tutorial, we will look at a functional way to define our endpoints in spring-webmvc and spring-webflux. Since functional endpoints are most useful and concise when working with Kotlin DSL, we will present our examples in the Kotlin language.

However, as we will see later, using Kotlin is not mandatory; in general, a similar functional approach is possible in Java.

2. Dependencies

Let's start with the dependencies that we will need to work. The latest versions can be found at Maven Central. In any case, we need the Kotlin standard library, that should be obvious. Now if we want to work with spring-webmvc we will need spring-boot-starter-web:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.3.4</version>
</dependency>

We use a starter here instead of a single spring-webmvc dependency to get the capabilities of spring-boot via the transitive spring-boot-starter. Also if we decide to work with spring-webflux we will need spring-boot-starter-webflux and several other dependencies to work with Kotlin coroutines:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    <version>3.3.4</version>
</dependency>
<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-coroutines-core</artifactId>
    <version>1.9.0</version>
</dependency>
<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-coroutines-reactor</artifactId>
    <version>1.9.0</version>
</dependency>

Now that we have the dependencies in hand, we can finally get started. But first we need to understand how functional bean definitions generally work in Spring.

3.1. Functional Bean Registration

There are many ways to create beans in spring-framework. The most famous examples are XML bean declarations, Java configurations, and customization through annotations. But there is another way to register beans in spring – functional. Let's look at an example:

@SpringBootApplication
open class Application

fun main(vararg args: String) {
    runApplication(*args) {
        addInitializers(
            beans {
                bean(
                  name = "functionallyDeclaredBean",
                  scope = BeanDefinitionDsl.Scope.SINGLETON,
                  isLazyInit = false,
                  isPrimary = false,
                  function = {
                      BigDecimal(1.0)
                  }
                )
            }
        )
    }
}

Here we launch the Spring Boot application through the runApplication() function and specify the Application class as the configuration source. This function actually takes two parameters – Java process arguments (args) and an extension function with SpringApplication as the receiver, since the last argument to runApplication is a function, it (the function) can be passed outside the parentheses. Let's briefly explain how the above code works.

3.2. DSL functions in Kotlin

Since the last parameter is a function type, we can put it out of brackets, which is what we did in our example. The fact that runApplication() has a function parameter with a specific receiver is very important here. Thanks to this receiver type (in our case, SpringApplication is also a receiver type), we can use the addInitializers() function inside the function body. Also extension functions with receiver are in fact a key feature of Kotlin, which allows not only to register beans as briefly as we saw above, but also to use DSL builders in Kotlin generally.

Thanks to these two features in Kotlin—extension of functions with receiver and transfer of functions outside the brackets—a DSL like the one above is possible. Now let's continue to study the example.

So, to simplify, the above code contains a chain of nested lambda functions with different receivers. These lambdas create a set of BeanDefinitions, in our case just one BeanDefinition to create a bean of type BigDecimal, which will be registered through the ApplicationContextInizilizer.

4. Functional endpoints with MVC

But the above code registers a normal bean in the context and has nothing to do with spring-webmvc or spring-webflux. To register an endpoint that will process HTTP requests, we need to call another lambda inside the router bean function:

beans {
    bean {
        router {
            GET("/endpoint/{country}") { it : ServlerRequest ->
                ServerResponse.ok().body(
                  mapOf(
                    "name" to it.param("name"),
                    "age" to it.headers().header("X-age")[0],
                    "country" to it.pathVariable("country")
                  )
                )
            }
        }
    }
}

Let's look at this example in more detail. Although lambdas with one parameter can access a parameter via it, we explicitly specify the router function parameter along with its type for demonstration purposes. The parameter is of type ServerRequest, which is an abstraction over the client's HTTP request. We can get any information from the request to process it, as in the example above – getting a request parameter, request header or path variable.

This approach is very similar to creating a RestController with a single method annotated with @GetMapping:

@RestController
class RegularController {
    @GetMapping(path = ["/endpoint/{country}"])
    fun getPerson(
      @RequestParam name: String,
      @RequestHeader(name = "X-age") age: String,
      @PathVariable country: String
    ): Map {
        return mapOf(
          "name" to name,
          "age" to age,
          "country" to country
        )
    }
}

In general, these two approaches are almost identical, for example HTTP filters for spring-security will work the same in both cases.

5. Origins of functional endpoints

It's important to understand that the router DSL function above is really just a convenient abstraction over RouterFunction API. This API exists for both spring-webmvc and spring-webflux modules. This means that any code that uses the router function DSL can also use the RouterFunction directly:

@Bean
open fun configure() {
    RouterFunctions.route()
      .GET("/endpoint/{country}") {
          ServerResponse.ok().body(
            mapOf(
              "name" to it.param("name"),
              "age" to it.headers().header("X-age")[0],
              "country" to it.pathVariable("country")
            )
          )
      }
    .build()
}

This will be completely identical to using the router DSL function. Note that we are not adding any initializers to the context. This is done intentionally to emphasize that it generally doesn't matter how we register our RouterFunction beans in the context – through the context initializer or Java Config.

6. Functional endpoints in Spring WebFlux

As already mentioned, spring-webflux has a functional approach to writing endpoints. Similar to spring-webmvc, we can either use the RouterFunction API directly or use the router function DSL abstraction. Let's quickly look at using the RouterFunction DSL directly:

@Bean
open fun configure(): RouterFunction {
    return RouterFunctions.route()
      .GET("/users/{id}") {
          ServerResponse
            .ok()
            .body(usersRepository.findUserById(it.pathVariable("id").toLong()))
      }
      .POST("/create") {
          usersRepository.createUsers(it.bodyToMono(User::class.java))
          return@POST ServerResponse
            .ok()
            .build()
      }
      .build()
}

This is quite similar to spring-webmvc and should be fairly straight forward. The DSL analogue for the router function will look like this:

@Bean
open fun endpoints() = router {
    GET("/users/{id}") {
        ServerResponse
          .ok()
          .body(usersRepository.findUserById(it.pathVariable("id").toLong()))
    }
    POST("/create") {
        usersRepository.createUsers(it.bodyToMono(User::class.java))
        return@POST ServerResponse
          .ok()
          .build()
    }
}

So this router function approach is more concise in Kotlin, simply because we can use it through a declarative DSL style. But, as is now clear, under the hood we can do exactly the same thing in Java.

7. Kotlin coroutines with functional endpoints

Finally, it's worth mentioning that Spring's functional endpoints also support Kotlin coroutines. Consider the following case:

@Bean
open fun registerForCo() =
    coRouter {
        GET("/users/{id}") {
            val customers : Flow<User> = usersRepository.findUserByIdForCoroutines(
              it.pathVariable("id").toLong()
            )
            ServerResponse.ok().bodyAndAwait(customers)
        }
    }

Here we use the coRouter DSL function. This is also an abstraction over the RouterFunction API, but this abstraction is built using suspend HandlerFunction. In other words, the lambda we pass in the GET is actually a suspend function, which in turn calls the findUserByIdForCoroutines method, which is also a suspend function.

Note that the return value of the findUserByIdForCoroutines method is Flow. This is important here since the coRouter DSL function is actually a wrapper over the reactive RouterFunction, not webmvc. Therefore, since Flow is an asynchronous cold flow that emits values ​​sequentially over time, it is broadly comparable to Publisher from the Reactor project. So under the hood, Spring simply does the conversion of the Flow to the Publisher of the Reactor project, and then the workflow is similar to the RouterFunction API in WebFlux.

8. Conclusion

In this article, we discussed the functional API for spring-webflux and spring-webmvc. It is based on the RouterFunction API, which itself differs between working with WebFlux (via DispatcherHandler) and working with WebMVC (via regular DispatcherServlet). RouterFunction can be used directly and there is no problem with that, but when working with Kotlin there is a more concise and elegant way to work with the RouterFunction API – through router DSL functions. This is just an abstraction over RouterFunction, made possible thanks to Kotlin extension functions with receiver and top level functions. It is also possible to work with coroutines and Kotlin DSL. The coroutine DSL is built on top of WebFlux's RouterFunction, so we need WebFlux to work with it.

As always, the source code for the MVC examples is available in this moduleand the source code for WebFlux is available Here.

Join the Russian-speaking community of Spring Boot developers in telegram – Spring IOto stay up to date with the latest news from the world of Spring Boot development and everything related to it.

We are waiting for everyone join us

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *