Reactor & Spring Boot Webflux series: EP 4 — Building Reactive REST APIs with Spring Boot WebFlux
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:
- Building Functional REST Endpoints with WebFlux: Learn how to craft RESTful endpoints using the powerful capabilities of Spring Boot WebFlux.
- 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.
- 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.
- 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 1 — Getting Started with Reactive Programming
- EP 2 — Functional Endpoints in Spring Boot WebFlux
- EP 3 — Working with Operators in Reactor
- >>> EP 4 — Building Reactive REST APIs with Spring Boot WebFlux <<<
- EP 5 — Error Handling and Testing in Reactive Programming (I’m working on it)
- EP 6 — Building a Reactive Microservice with Spring Cloud (Coming soon)
- EP 7 — Integrating Reactive Databases with Spring Boot WebFlux (Coming soon)
- EP 8 — Real-World Examples and Best Practices (Coming soon)
- EP 9 — Conclusion and Next Steps (Coming soon)
Key Topics
- Creating REST Endpoints with Spring Boot WebFlux
- Handling Requests and Responses Using Reactive Streams
- Data Manipulation with Fake Repository
- Refactor — Don’t Repeat Yourself (DRY)
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, inTask.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
, andstatus
(enum) properties.
TaskModel
Data Class:
- Represents the model of a task, typically used for storage in a database.
- Extends
BaseEntity
and includes properties forid
,title
, andstatus
.
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 theTaskHandler
class. - We use the
taskService.all()
method, which asynchronously retrieves all tasks. Then map it toTaskDTO
. - The response is created using
ServerResponse.ok().bodyAndAwait(...)
, ensuring it's a successful response. - The
bodyAndAwait
method takes aFlow<TaskDTO>
(tasksResponse
) input, which is obtained by converting the originalFlux<TaskDTO>
into aFlow<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:
service.all().map(Task::toDTO)
: This part fetches all tasks asynchronously from the service (service.all()
). Themap
function then transforms these tasks intoTaskDTO
objects, maintaining the asynchronous nature of the operation..asFlow()
: After transforming the tasks, theasFlow()
extension function is applied. This is where the transition fromFlux<TaskDTO>
(reactive streams) toFlow<TaskDTO>
(Kotlin Coroutines) happens. TheasFlow()
extension function is provided by thekotlinx.coroutines.reactive
standard library, and it adapts reactive types (likeFlux
in Reactor) to Kotlin Coroutines.ServerResponse.ok().bodyAndAwait(tasksResponse)
: Finally, the response is constructed with a status of OK (200
) and a body that awaits the completion of theFlow<TaskDTO>
(bodyAndAwait(tasksResponse)
). The use ofawait
here is essential in the context of Kotlin Coroutines, allowing the response to be suspended until theFlow
is fully collected.
Note:
As much as I known right now, normally,Flow
is used for thebodyAndAwait()
method.
- For thebodyValue
andbodyValueAndAwait
, use only for the actual value that is in the Publisher (Flux
andMono
). For exampleString
,Int
, orTaskDTO
.
- For thebody
, use only for the Publisher that wrap the actual value. For exampleMono<String>
,Flux<Int>
, orFlux<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 successfulServerResponse
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()
andrequest.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 theTaskService
class. all()
method fetches all tasks from our repository using thefindAll()
method.- The
map()
method then transforms these tasks into our domain model (Task
). - Then return the
Flux
ofTask
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 therepository.findById()
.- The
map
function helps us convert from theTaskModel
to our domain model (Task
). - This operation returns a
Mono
, representing a potentially asynchronous single result ofTask
.
Create task:
// TaskService.kt
fun create(task: Task): Mono<Task> {
val createdTask = repository.save(task.toModel())
return createdTask.map(Task::fromModel)
}
create()
method retrieves theTask
(task details), then we convert theTask
to aTaskModel
that suits our fake repository usingtoModel()
.- The
repository.save()
stores thisTaskModel
to the fake database. - The resulting created task is mapped from
TaskModel
to our domain model (Task
) using themap
function and returned as aMono
ofTask
.
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 aTask
that transforms toTaskModel
taskToUpdate
is the existing task that we get from the fake database. Then we update some details (title
andstatus
) from the request body (Task
).- Finally, we return the updated task back to the repository using (
repository.save()
), and the result is mapped from theTaskModel
to ourTask
(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 providedaction
(either"next"
or"previous"
).- It returns a function that given a task ID, updates its status according to
nextStatus()
orpreviousStatus()
. nextStatus()
andpreviousStatus()
methods will update the task status usingrepository.save()
. They update the status and transform theTask
toTaskModel
, 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 therepository.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
andresponseCreatedWithDTO
: 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 theid
then parse toLong
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 theid
then parse toLong
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 theid
then parse toLong
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 theid
then parse toLong
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 👈