Reactor & Spring Boot Webflux series: EP 4 — Building Reactive REST APIs with Spring Boot WebFlux

fResult
18 min readJan 14, 2024

--

Recap: EP 3 — Working with Operators in Reactor
In our last episode, we dived deep into the world of Reactor, exploring essential operators that empower our reactive programming journey.
We transformed, filtered, aggregated, and merged data streams using tools like map, filter, flatMap, and more.
These operators are like a craftsman’s toolkit and provide expressive ways to manipulate data within reactive streams.

What to Expect in EP 4 — Building Reactive REST APIs with Spring Boot Webflux
Buckle up for the next leg of our coding journey!
In Episode 4, we’ll explore building Reactive REST APIs with Spring Boot WebFlux.

Here’s a sneak peek into the key topics:

  1. Building Functional REST Endpoints with WebFlux: Learn how to craft RESTful endpoints using the powerful capabilities of Spring Boot WebFlux.
  2. Handling Reactive Requests and Responses: Explore the world of handling requests and responses effortlessly using reactive streams. Delve into the asynchronous world with Functional Endpoints, understanding how to efficiently process incoming requests and respond asynchronously.
  3. Data Manipulation with Fake Repository: Dive into data manipulation by simulating a fake repository for our TodoList App. Experience the power of reactive streams as we demonstrate CRUD operations — creating, reading, updating, and deleting tasks — without a real database.
  4. Refactor — Make it DRY: Discover the art of code improvement using the “Don’t Repeat Yourself” (DRY) principle. Learn how to make our code more maintainable and efficient by getting rid of redundancy using Functional Programming code styles. We’ll explore ways to enhance code clarity through refactoring.

Get ready to transform our REST API game with the reactive approach!

Journey Map.

EP 0 — A Series of Reactive Programming with Reactor & Spring Boot Webflux (The Road to Backend Developer Edition)

Key Topics

These repositories are my final code for this episode.
Feel free to check one of them out.

On GitHub

On Gitlab

Checkout ep04_building-reactive-rest-apis-with-spring-boot-webflux branch.

git checkout --force --detach ep04_building-reactive-rest-apis-with-spring-boot-webflux
# OR
git switch ep04_building-reactive-rest-apis-with-spring-boot-webflux

Creating REST endpoints with Spring Boot WebFlux

In this episode, we’re diving into creating REST endpoints using Spring Boot WebFlux.
We’re sticking to the functional style, saying goodbye to the traditional @RestController.
If you remember from episode 2, we used Functional Endpoints with coRouter in Kotlin.
We’ll be using a TodoList App as our practical example.

Disclamer:
Some parts of code in this episode may not be a good practice.
For example, in Task.kt file.
I do Prove of Concept (PoC) about data modelling following FP style.

Again, why go functional?

Remember in our past episodes (EP 2 — Functional Endpoints), we said goodbye to the traditional @RestController and welcomed the flexibility of functional-style routing.
With coRouter, we entered the world of asynchronous programming and discovered the power of Kotlin Coroutines.

You can check out the different of current and previous code here.
> > >
Code Changes < < <

Create TaskRouter.kt file:

touch src/main/kotlin/dev/fresult/reactiveweb/routers/TaskRouter.kt

Add Task Routes:

package dev.fresult.reactiveweb.routers

@Configuration
class TaskRouter(private val handler: TaskHandler) {
@Bean
fun taskRoutes() = coRouter {
"/tasks".nest {
GET("", handler::all)
GET("/{id}", handler::byId)
POST("", accept(MediaType.APPLICATION_JSON), handler::create)
PUT("/{id}", handler::updateById)
PATCH("/{id}/{status-action}", handler::updateStatusById)
DELETE("/{id}", handler::deleteById)
}
}
}

This snippet sets up routes for common CRUD operations — getting all tasks, getting a task by ID, creating a new task, updating a task, and deleting a task.
Each route is linked to a corresponding handler function (e.g., handler::byId).

Prepare Handlers

Create TaskHandler.kt file:

touch src/main/kotlin/dev/fresult/reactiveweb/handlers/TaskHandler.kt

Add Task handler’s methods:

package dev.fresult.reactiveweb.handlers

@Component
class TaskHandler {
suspend fun all(request: ServerRequest): ServerResponse {
TODO()
}

suspend fun byId(request: ServerRequest): ServerResponse {
val id: Long = request.pathVariable("id").toLong()
TODO()
}

suspend fun create(request: ServerRequest): ServerResponse = {
TODO()
}

suspend fun updateById(request: ServerRequest): ServerResponse {
val id: Long = request.pathVariable("id").toLong()
TODO()
}

suspend fun updateStatusById(request: ServerRequest): ServerResponse {
val id: Long = request.pathVariable("id").toLong()
val action = request.pathVariable("status-action")
TODO()
}

suspend fun deleteById(request: ServerRequest): ServerResponse {
val id: Long = request.pathVariable("id").toLong()
TODO()
}
}

In these code snippets, we retrieve the id from the path parameter /{id} and the action from the /{status-action} specified in the routes of the TaskRouter class.
This process is similar to what we have discussed in Episode 2 under the Handling Requests and Responses topic.
After getting the id and action, we use TODO() for now, indicating that we haven’t implemented these methods yet.

Prepare the TaskService class

Create Service.kt file:

mkdir src/main/kotlin/dev/fresult/reactiveweb/services
# THEN
touch src/main/kotlin/dev/fresult/reactiveweb/services/TaskService.kt

Add Task service’s methods:

package dev.fresult.reactiveweb

@Service
class TaskService(val repository: TaskRepository) {
fun all(): Flux<Task> = TODO()

fun byId(id: Long): Mono<Task> = TODO()

fun create(task: Task): Mono<Task> = TODO()

/**
* @param action "next" | "previous"
* @return Mono<Task>
*/
fun updateByAction(action: String): (Long) -> Mono<Task> =
if (action == "next") ::nextStatus else ::previousStatus

fun updateById(id: Long, task: Task): Mono<Task> = TODO()

fun deleteById(id: Long) = TODO()

private fun nextStatus(id: Long): Mono<Task> = TODO()

private fun previousStatus(id: Long): Mono<Task> = TODO()
}

We’ve introduced methods to the TaskService class, expanding its functionality.
However, it's worth noting that most of the methods currently have TODO() and need actual implementations.

One notable method is updateByAction, which has a distinctive function signature: (String) -> (Long) -> Mono<Task>.
In simpler terms, it takes an action string ("next" or "previous") and returns a function that, in turn, takes an id and produces a reactive Mono<Task>.
To be more specific, it can be understood as ("next" | "previous") -> (ID) -> Mono<Task>.

Create Data Models

Create Task.kt file:

mkdir src/main/kotlin/dev/fresult/reactiveweb/entities
# THEN
touch src/main/kotlin/dev/fresult/reactiveweb/entities/Task.kt

Add these data models to the file:

package dev.fresult.reactiveweb.entities

sealed class Task(open val id: Long, open val title: String) {
data class Todo(override val id: Long, override val title: String) : Task(id, title)
data class Doing(override val id: Long, override val title: String) : Task(id, title)
data class Done(override val id: Long, override val title: String) : Task(id, title)

fun toDTO(): TaskDTO {
return when (this) {
is Todo -> TaskDTO(id, title, TaskStatus.TODO)
is Doing -> TaskDTO(id, title, TaskStatus.DOING)
is Done -> TaskDTO(id, title, TaskStatus.DONE)
}
}

fun toModel(): TaskModel {
return when (this) {
is Todo -> TaskModel(id, title, TaskStatus.TODO.name)
is Doing -> TaskModel(id, title, TaskStatus.DOING.name)
is Done -> TaskModel(id, title, TaskStatus.DONE.name)
}
}

companion object {
fun fromDTO(dto: TaskDTO): Task {
return when (dto.status) {
TaskStatus.TODO -> Todo(getId(dto.id), dto.title)
TaskStatus.DOING -> Doing(getId(dto.id), dto.title)
TaskStatus.DONE -> Done(getId(dto.id), dto.title)
}
}

fun fromModel(model: TaskModel): Task {
return when (model.status) {
TaskStatus.TODO.name -> Todo(getId(model.id), model.title)
TaskStatus.DOING.name -> Doing(getId(model.id), model.title)
TaskStatus.DONE.name -> Done(getId(model.id), model.title)
else -> throw Exception("Task Status may be wrong.")
}
}
}
}

fun getId(id: Long? = null): Long = id.takeIf { it !== null } ?: (Math.random() * 10000).toLong()

open class BaseEntity<ID>(open val id: ID?)
data class TaskModel(
override val id: Long? = null,
val title: String,
val status: String,
) : BaseEntity<Long>(id)

data class TaskDTO(
val id: Long? = null,
val title: String,
val status: TaskStatus,
)

enum class TaskStatus {
TODO, DOING, DONE
}

The code snippet above defines a simple entity hierarchy, DTO (Data Transfer Object), and model which is represented in the table in the database for our TodoList Application.

Here’s a brief breakdown:

Task Sealed Class:

  • Represents different states of a task (Todo, Doing, Done (data classes)).
  • Has functions (toDTO, toModel) to convert tasks to DTO and model representations.
  • A companion object with functions (fromDTO, fromModel) to create tasks from DTO and model.

TaskDTO Data Class:

  • Represents the Data Transfer Object for a task, normally used for communication with the frontend app, or the other service.
  • Includes id, title, and status (enum) properties.

TaskModel Data Class:

  • Represents the model of a task, typically used for storage in a database.
  • Extends BaseEntity and includes properties for id, title, and status.

BaseEntity Class:

  • An open class representing the base entity with an id. Used for common properties in entities.

TaskStatus Enum:

  • Enumerates the possible states of a task: TODO, DOING, DONE.

getId Function:

  • Helper function to get an ID. If an ID is provided, it’s used; otherwise, a random ID is generated.

Next, we will handle requests and responses in these CRUD operations that we set up earlier.

TodoList App Scenario

Imagine you have a simple TodoList App where we can effortlessly handle our tasks.

In this application, you can…

  • Fetch all tasks
  • Retrieve a specific one by ID
  • Add new tasks
  • Update their details
  • Update their status (with the next and the previous action)
  • Delete them

It’s all about managing our task list seamlessly.
Next, we’ll manage requests and responses, including some error handling.

Handling Requests and Responses Using Reactive Streams

In this topic, we’ll explore how reactive streams efficiently handle incoming requests and provide asynchronous responses in our TodoList App.

You can check out the different of current and previous code here.
> > >
Code Changes < < <

First of all, we will create TaskRepository which assumes it connects to the database

Create TaskRepository.kt file:

mkdir src/main/kotlin/dev/fresult/reactiveweb/repositories
# THEN
touch src/main/kotlin/dev/fresult/reactiveweb/repositories/TaskRepository.kt

Add these methods to the TaskRepository class:

package dev.fresult.reactiveweb.repositories

@Component
class TaskRepository {
private var tasks: Flux<TaskDAO> = Flux.just(
TaskDAO(777, "Kill Tanos", TaskStatus.TODO.name),
TaskDAO(888, "Catch John Wick", TaskStatus.DOING.name),
TaskDAO(999, "Destroy The Matrix", TaskStatus.DONE.name),
)


fun findAll(): Flux<TaskModel> = tasks

fun findById(id: Long): Mono<TaskModel> {
return tasks.filter(isSameId(id)).switchIfEmpty {
throw NoSuchElementException("Task ID $id not found")
}.next()
}

fun save(task: TaskModel): Mono<TaskModel> {
val taskIdToSave = task.id ?: getId()
val taskToSave = if (Optional.ofNullable(task.id).isEmpty) task
else task.copy(id = taskIdToSave, title = task.title, status = task.status)
tasks = tasks.filter(not(isSameId(taskIdToSave))).concatWith(taskToSave.copy(id = taskIdToSave).toMono())

return findById(taskIdToSave)
}

fun deleteById(id: Long) {
tasks = tasks.filter(not(isSameId<TaskModel, Long>(id)))
}
}


typealias PredicateFn<T> = (T) -> Boolean
private fun <T : BaseEntity<ID>, ID> isSameId(id: ID): PredicateFn<T> {
return { item -> item.id == id }
}

In this code above, we’re creating a pretend database (not a real one) using @Component (instead of @Repository.)
It's like we're playing with a make-believe repository.
We have a list called tasks with some example tasks inside.

Here’s a quick explanation:

  • tasks: It’s like a list of tasks we made up, not a real database.
  • findAll(): It gives you all the tasks in our pretend database.
  • findById(): We can find a specific task by its ID. If the ID isn’t there, we say it’s not found.
  • save(): We can add a new task or update an existing one. If it’s new, we make a new ID for it.
  • deleteById(): We can remove a task by its ID.

Handling requests

Find all tasks:

@Component
class TaskHandler(private val service: TaskService) {
suspend fun all(request: ServerRequest): ServerResponse {
val tasksResponse = service.all().map(Task::toDTO).asFlow()
return ServerResponse.ok().bodyAndAwait(tasksResponse)
}
.
.
.
}
  • We have a service instance, private val service: TaskService, injected into the TaskHandler class.
  • We use the taskService.all() method, which asynchronously retrieves all tasks. Then map it to TaskDTO.
  • The response is created using ServerResponse.ok().bodyAndAwait(...), ensuring it's a successful response.
  • The bodyAndAwait method takes a Flow<TaskDTO> (tasksResponse) input, which is obtained by converting the original Flux<TaskDTO> into a Flow<TaskDTO> using the .asFlow() method.

More information relates to Flow:
In the given code snippet, Flow<TaskDTO> is used in the context of Kotlin Coroutines.
Let's break down how it relates:

  1. service.all().map(Task::toDTO): This part fetches all tasks asynchronously from the service (service.all()). The map function then transforms these tasks into TaskDTO objects, maintaining the asynchronous nature of the operation.
  2. .asFlow(): After transforming the tasks, the asFlow() extension function is applied. This is where the transition from Flux<TaskDTO> (reactive streams) to Flow<TaskDTO> (Kotlin Coroutines) happens. The asFlow() extension function is provided by the kotlinx.coroutines.reactive standard library, and it adapts reactive types (like Flux in Reactor) to Kotlin Coroutines.
  3. ServerResponse.ok().bodyAndAwait(tasksResponse): Finally, the response is constructed with a status of OK (200) and a body that awaits the completion of the Flow<TaskDTO> (bodyAndAwait(tasksResponse)). The use of await here is essential in the context of Kotlin Coroutines, allowing the response to be suspended until the Flow is fully collected.

Note:
As much as I known right now, normally, Flow is used for the bodyAndAwait() method.
- For the bodyValue and bodyValueAndAwait, use only for the actual value that is in the Publisher (Flux and Mono). For example String, Int, or TaskDTO.
- For the body, use only for the Publisher that wrap the actual value. For example Mono<String>, Flux<Int>, or Flux<TaskDTO>.

> Note in January 2024 <

Asynchronous Responses and error handling

Find task by ID:

// TaskHandler.kt
suspend fun byId(request: ServerRequest): ServerResponse {
val id: Long = request.pathVariable("id").toLong()
return service.byId(id).flatMap {
ServerResponse.ok().bodyValue(it.toDTO())
}.onErrorResume { exception ->
when (exception) {
is NoSuchElementException -> ServerResponse.status(HttpStatus.NOT_FOUND).bodyValue(exception.message.orEmpty())
else -> ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).bodyValue("Some thing went wrong")
}
}.awaitSingle()
}
  • We extract the task ID from the path variable (request.pathVariable("id").toLong().)
  • The service.byId(id) method asynchronously retrieves the task based on the ID.
  • We use flatMap to handle the case when the task is found, responding with a successful ServerResponse containing the task's details.
  • To handle errors, we use the onErrorResume method. If an error occurs (e.g., NoSuchElementException), we respond with a 404 Not Found status. If it's a different type of error, we respond with a 500 Internal Server Error status.

Create task:

// TaskHandler.kt
suspend fun create(request: ServerRequest): ServerResponse {
// Extract the task details from the request body
return request.bodyToMono<TaskDTO>().flatMap { body ->
// Use the service to create a new task based on the provided details
service.create(Task.fromDTO(body)).flatMap {
// Respond with a successful ServerResponse containing the created task's details
ServerResponse.status(HttpStatus.CREATED).bodyValue(it.toDTO())
}
}.switchIfEmpty {
// Handle the case when the request body is null, responding with a bad request status
ServerResponse.badRequest().bodyValue("Request Body cannot be null")
}.awaitSingle()
}
  • We get the task details from the request body (request.bodyToMono<TaskDTO>().)
  • Using the service.create(), we create a new task with the provided details.
  • If successful, we respond with a status of 201 Created and include the details of the created task as a response body.
  • If the request body is null, we handle it by responding with a status of 400 Bad Request and "Request Body cannot be null" as a response body.

Update task by ID:

// TaskHandler.kt
suspend fun updateById(request: ServerRequest): ServerResponse {
val id: Long = request.pathVariable("id").toLong()
return request.bodyToMono<TaskDTO>().flatMap { body ->
service.updateById(id, Task.fromDTO(body)).flatMap { updatedTask ->
ServerResponse.ok().bodyValue(updatedTask.toDTO())
}.onErrorResume { exception ->
when (exception) {
is NoSuchElementException ->
ServerResponse.status(HttpStatus.NOT_FOUND).bodyValue(exception.message.orEmpty())
else -> ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).bodyValue("Something went wrong")
}
}
}.switchIfEmpty {
ServerResponse.badRequest().bodyValue("Request Body cannot be null")
}.awaitSingle()
}
  • We get the task ID from the path variable (request.pathVariable("id").toLong().)
  • Extract the updated task details from the request body (request.bodyToMono<TaskDTO>().)
  • Using the service, update the task based on the provided details (service.updateById().)
  • If successful, respond with a status of 200 OK and include the updated task’s details as a response body.
  • If the task is not found, respond with a status of 404 Not Found with the exception (error) message.
  • If there’s a different error, respond with a status of 500 Internal Server Error with text "Something went wrong" as a response body.
  • Handle the case when the request body is null, responding with a status of 400 Bad Request.

Update task’s status by ID:

// TaskHandler.kt
suspend fun updateStatusById(request: ServerRequest): ServerResponse {
val id: Long = request.pathVariable("id").toLong()
val action = request.pathVariable("status-action")
val updateStatusById = service.updateByAction(action)

return updateStatusById(id).flatMap { updatedTask ->
ServerResponse.ok().bodyValue(updatedTask.toDTO())
}.onErrorResume { exception ->
when (exception) {
is NoSuchElementException -> ServerResponse.status(HttpStatus.NOT_FOUND).bodyValue(exception.message.orEmpty())
else -> ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).bodyValue("Something went wrong")
}
}.awaitSingle()
}
  • We get the task ID and action from the path variables (request.pathVariable("id").toLong() and request.pathVariable("status-action")). The action (status-action) can be "next" or "previous".
  • Use the service to update the task’s status based on the provided action (service.updateByAction(action)).
  • If successful, respond with a status of 200 OK and include the updated task’s details as a response body.
  • If the task is not found, respond with a status of 404 Not Found.
  • If there’s a different error, respond with a status of 500 Internal Server Error.

Delete task by ID:

// TaskHandler.kt
suspend fun deleteById(request: ServerRequest): ServerResponse {
val id: Long = request.pathVariable("id").toLong()
service.deleteById(id)
return ServerResponse.noContent().buildAndAwait()
}
  • We get the task ID from the path variable.
  • Use the service to delete the task by ID (service.deleteById().)
  • Regardless of whether the deletion is successful or not, we respond with a status of 204 No Content.

Data Manipulating with Fake Repository

You can check out the different of current and previous code here.
> > >
Code Changes < < <

Task Repository

We’ve already added this class and its methods in the previous topic — Handling Requests and Responses Using Reactive Streams.

Task Service

Before we go to the TaskService class, let’s recall about TaskModel data class again.

open class BaseEntity<ID>(open val id: ID?)
data class TaskModel(
override val id: Long? = null,
val title: String,
val status: String,
) : BaseEntity<Long>(id)

TaskModel is the data model that represents the shape of data in the database.
So, in the TaskService class, we will see mapping from Task to TaskModel and mapping from TaskModel to Task several times.

Fetching all tasks:

// TaskService.kt
@Service
class TaskService(private val repository: TaskRepository) {
fun all(): Flux<Task> {
return repository.findAll().map(Task::fromModel)
}

.
.
.
}
  • We have a repository instance, private val repository: TaskRepository, injected into the TaskService class.
  • all() method fetches all tasks from our repository using the findAll() method.
  • The map() method then transforms these tasks into our domain model (Task).
  • Then return the Flux of Task that can be consumed asynchronously.

Fetching task by ID:

// TaskService.kt
fun byId(id: Long): Mono<Task> {
return repository.findById(id).map(Task::fromModel)
}
  • byId() method retrieves a task by its unique ID from the repository using the repository.findById().
  • The map function helps us convert from the TaskModel to our domain model (Task).
  • This operation returns a Mono, representing a potentially asynchronous single result of Task.

Create task:

// TaskService.kt
fun create(task: Task): Mono<Task> {
val createdTask = repository.save(task.toModel())
return createdTask.map(Task::fromModel)
}
  • create() method retrieves the Task (task details), then we convert the Task to a TaskModel that suits our fake repository using toModel().
  • The repository.save() stores this TaskModel to the fake database.
  • The resulting created task is mapped from TaskModel to our domain model (Task) using the map function and returned as a Mono of Task.

Update task by ID:

// TaskService.kt
fun updateById(id: Long, task: Task): Mono<Task> {
return byId(id).flatMap { existingTask ->
val taskFromBody = task.toModel()
val taskToUpdate = existingTask.toModel().copy(
id = id,
title = taskFromBody.title,
status = taskFromBody.status,
)
repository.save(taskToUpdate).map(Task::fromModel)
}
}
  • updateById() method retrieves the task ID (Long), and the task details (Task).
  • taskFromBody is a Task that transforms to TaskModel
  • taskToUpdate is the existing task that we get from the fake database. Then we update some details (title and status) from the request body (Task).
  • Finally, we return the updated task back to the repository using (repository.save()), and the result is mapped from the TaskModel to our Task (Mono<Task>).

Update task’s status by Action and ID:

// TaskService.kt
/**
* @param action "next" | "previous"
* @return Mono<Task>
*/
fun updateByAction(action: String): (Long) -> Mono<Task> =
if (action == "next") ::nextStatus else ::previousStatus

private fun nextStatus(id: Long): Mono<Task> {
return byId(id).flatMap { existingTask ->
val taskToUpdate = when (existingTask) {
is Task.Todo -> Task.Doing(existingTask.id, existingTask.title)
is Task.Doing -> Task.Done(existingTask.id, existingTask.title)
else -> Task.Done(existingTask.id, existingTask.title)
}

repository.save(taskToUpdate.toModel()).map(Task::fromModel)
}
}

private fun previousStatus(id: Long): Mono<Task> {
return byId(id).flatMap { existingTask ->
val taskToUpdate = when (existingTask) {
is Task.Done -> Task.Doing(existingTask.id, existingTask.title)
is Task.Doing -> Task.Todo(existingTask.id, existingTask.title)
else -> Task.Todo(existingTask.id, existingTask.title)
}

repository.save(taskToUpdate.toModel()).map(Task::fromModel)
}
}
  • updateByAction() method used for updating task status based on the provided action (either "next" or "previous").
  • It returns a function that given a task ID, updates its status according to nextStatus() or previousStatus().
  • nextStatus() and previousStatus() methods will update the task status using repository.save(). They update the status and transform the Task to TaskModel, then return the updated task (Mono<Task>).

Delete task by ID:

// TaskService.kt
fun deleteById(id: Long) {
return repository.deleteById(id)
}
  • deleteById() method, we simply call the repository.deleteById(), passing the unique ID of the task to be deleted.
  • It returns the Mono<Unit>.

Refactor — Don’t Repeat Yourself (DRY)

You can check out the different of current and previous code here.
> > >
Code Changes < < <

Here are all of the duplicated codes that I can create as the new functions, and then re-used in the TaskHandler class.

private fun idFromParam(request: ServerRequest) = request.pathVariable("id").toLong()

private val responseOkWithDTO = responseOneWithDTO(HttpStatus.OK)
private val responseCreatedWithDTO = responseOneWithDTO(HttpStatus.CREATED)
private fun responseOneWithDTO(status: HttpStatus): (Task) -> Mono<ServerResponse> = { task ->
ServerResponse.status(status).bodyValue(task.toDTO())
}

private fun badRequestErrorWhenBodyIsEmpty(): Mono<ServerResponse> =
ServerResponse.badRequest().bodyValue("Request Body cannot be null")

private fun notFoundOrServerError(exception: Throwable): Mono<ServerResponse> = when (exception) {
is NoSuchElementException -> ServerResponse.status(HttpStatus.NOT_FOUND).bodyValue(exception.message.orEmpty())
else -> ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).bodyValue("Some thing went wrong")
}
  • idFromParam: Extracts the ID from the request path.
  • responseOkWithDTO and responseCreatedWithDTO: Pre-configured response functions.
  • responseOneWithDTO: Generic response function with a specified HTTP status.
  • badRequestErrorWhenBodyIsEmpty: Returns a bad request response when the request body is empty.
  • notFoundOrServerError: Handles exceptions, responding with either a not found or server error.

Fetching Task by ID

// TaskHandler.kt
suspend fun byId(request: ServerRequest): ServerResponse {
val id = idFromParam(request)
return service.byId(id).flatMap(::okResponseOneWithDTO)
.onErrorResume(::notFoundOrServerError).awaitSingle()
}
  • Invoke the reusable function idFromParam which extracts the id then parse to Long data type.
  • Utilizes responseOkWithDTO for a cleaner response configuration.
  • Use ::notFoundOrServerError for cleaner error handling.

Creating Task

// TaskHandler.kt
suspend fun create(request: ServerRequest): ServerResponse {
return request.bodyToMono<TaskDTO>().flatMap { body ->
service.create(Task.fromDTO(body)).flatMap(responseCreatedWithDTO)
}.switchIfEmpty(::badRequestErrorWhenBodyIsEmpty).awaitSingle()
}
  • Utilize responseOkWithDTO for a cleaner response configuration.
  • Use ::notFoundOrServerError for cleaner error handling.

Updating Task’s details by ID

// TaskHandler.kt
suspend fun updateById(request: ServerRequest): ServerResponse {
val id = idFromParam(request)
return request.bodyToMono<TaskDTO>().flatMap { body ->
service.updateById(id, Task.fromDTO(body)).flatMap(responseOkWithDTO)
.onErrorResume(::notFoundOrServerError)
}.switchIfEmpty(::badRequestErrorWhenBodyIsEmpty).awaitSingle()
}
  • Invoke the reusable function idFromParam which extracts the id then parse to Long data type.
  • Utilize responseOkWithDTO for a cleaner response configuration.
  • Use ::notFoundOrServerError for cleaner error handling.
  • Utilize reusable badRequestErrorWhenBodyIsEmpty function for a cleaner when the body request is null.

Updating Task’s status by ID

// TaskHandler.kt
suspend fun updateStatusById(request: ServerRequest): ServerResponse {
val id = idFromParam(request)
val action = request.pathVariable("status-action")
val updateStatusById = service.updateByAction(action)

return updateStatusById(id).flatMap(responseOkWithDTO)
.onErrorResume(::notFoundOrServerError).awaitSingle()
}
  • Invoke the reusable function idFromParam which extracts the id then parse to Long data type.
  • Utilize responseOkWithDTO for a cleaner response configuration.
  • Use ::notFoundOrServerError for cleaner error handling.

Deleting Task by ID

// TaskHandler.kt
suspend fun deleteById(request: ServerRequest): ServerResponse {
val id = idFromParam(request)
service.deleteById(id)
return ServerResponse.noContent().buildAndAwait()
}
  • Invoke the reusable function idFromParam which extracts the id then parse to Long data type.

🎉 Hooray! 🎉
These refactored methods enhance code readability, maintainability, and consistency.

Conclusion

In this episode, we start on a journey through the creation and refinement of a reactive web application using Spring Boot WebFlux.
We talk about basic ideas, like creating REST endpoints and handling requests and responses using reactive streams.

In the first topic, “Introduction to Reactive Web Application,” we looked at the basics of reactive programming and set the stage for building our application.
We paved the road to understanding the asynchronous and non-blocking nature of reactive systems.

Moving on to “Creating REST Endpoints with Spring Boot WebFlux,” we crafted endpoints to handle different jobs.
These jobs included fetching all tasks, retrieving a task by ID, creating new tasks, updating task details, updating task status, and deleting tasks.
We used reactive streams to do these tasks in a smart way.

In the third topic, “Data Manipulation with Fake Repository,” we explored the implementation details of the application’s data layer.
We zoomed into the Task Repository and Task Service, focusing on fetching, creating, updating, and deleting tasks.
The explanations made it easy to see how data changes happen in our app.

In our final topic, “Refactor — Don’t Repeat Yourself (DRY),” we got rid of code duplication in the TaskHandler class.
We created reusable functions, making the code more readable and maintainable.
Each improved method, like getting and making tasks or fixing and erasing them, got better at handling responses and mistakes.

What’s Next?

As we conclude this episode, consider expanding your knowledge.
You can check out advanced Spring Boot stuff, learn more about reactive programming, or try out related technologies.
Continuously learning and practising will help you handle harder problems in your coding adventures.

Stay curious, keep coding, and enjoy creating software that makes a positive impact. Happy coding! 🚀

Thanks for reading until the final line.
If you’re confused or have any questions, don’t hesitate to leave the comments.

Feel free to connect with me and stay updated on the latest insights and discussions.
👉 https://linkedin.com/in/fResult 👈

--

--

fResult
fResult

Written by fResult

ชื่อเล่นว่ากร เขามี background มาจากอาชีพเด็กวิ่งเอกสารในอาคารของธนาคาร โดยเรียนไปด้วยจนจบจากมหาลัยเปิดแห่งหนึ่ง และปัจจุบันทำงานเป็น Web Developer ครับทั่นน