Creating Your Reactive REST API with Kotlin and Ktor Part I

José Luis González Sánchez
Hyperskill
Published in
11 min readMay 16, 2023

Introduction

One of the essential things that any backend developer must master is creating REST services.

At present, the management of resources efficiently and effectively is a plus that we must achieve. Reactive programming offers us solutions to the demand for faster response times and high availability of the systems, characteristics achieved with previous models of Microservices, but giving solutions to the problems of excessive use of CPU, blocks in input and output operations, or on memory usage (due to large thread pools) that these models suffered from.

Throughout this series of articles, a series of contents are presented that you will learn through the different Hyperskill training tracks. With Hyperskill you will be able to deepen them, expand them, analyze different alternatives, and become a true backend developer.

In these tutorials we show how to set up a reactive REST API using Ktor and Kotlin, analyzing all the elements, using Railway Oriented Programming, dependencies injection, testing and applying various security, authentication, and authorization configurations until you can deploy it and enjoy your achievements. Remember that this code is pedagogical, and shows many of the contents that you will learn in Hyperskill in a didactic and easy-to-read way. It is not intended to create the best production code in real environments.

Creating Your Reactive REST API with Kotlin and Ktor Part I
Creating Your Reactive REST API with Kotlin and Ktor Part I

Reactive Programming

Following the principles of the Reactive Manifesto, reactive systems must be:

  • Responsive: The system responds in a timely manner if at all possible.
  • Resilient: The system stays responsive in the face of failure.
  • Elastic: The system stays responsive under varying workloads.
  • Message Driven: Reactive Systems rely on asynchronous message-passing to establish a boundary between components that ensures loose coupling, isolation, and location transparency.
Reactive programming focuses on working with asynchronous streams from finite or infinite data sources where we can observe them.
Figure by https://www.reactivemanifesto.org/

Reactive programming focuses on working with asynchronous streams from finite or infinite data sources where we can observe them.

Reactive programming is a subset of asynchronous programming. It supports decomposing the problem into multiple discrete steps where each can be executed in an asynchronous and non-blocking way and then be composed to produce the final workflow.

Reactive Programming offers several advantages, including improved utilization of computing resources on multi-core and multi-CPU systems and enhanced performance by reducing serialization points. Another significant benefit is increased developer productivity. Traditional programming paradigms have struggled to provide a straightforward and maintainable way of dealing with asynchronous and non-blocking computation and I/O. Reactive Programming solves most of these challenges by eliminating the need for explicit coordination between active components.

To leverage asynchronous execution, back-pressure must be included to avoid over-utilization or unbounded consumption of resources. For example, in inputs and outputs with databases using JDBC, we block the thread until we get a response. This is a simple example of the things that we are going to solve to improve the productivity of our services.

For example, in inputs and outputs with databases using JDBC, we block the thread until we get a response. This is a simple example of the things that we are going to solve to improve the productivity of our services.
Figure by https://kb.novaordis.com/index.php/Reactive_Programming

This is where Ktor and Kotlin are an invincible team. Ktor offers us the possibility of creating asynchronous services (first condition), and Kotlin offers coroutines and Flows to process collections asynchronously and reactively.

Creating a new project with Kotlin and Ktor

Kotlin is a statically typed programming language that has several advantages over other programming languages. Some of the advantages of Kotlin are: interoperability with Java, concise syntax, null safety, functional programming support, extension functions, and coroutines which makes it easier to write asynchronous and non-blocking code. Coroutines are lightweight and efficient and can be used to simplify complex asynchronous code. Kotlin support is a great choice for building modern applications.

Ktor is a Kotlin-based lightweight framework used to create server-side applications and web services. It provides a simple and flexible API for building asynchronous, event-driven, and non-blocking applications.

With Ktor, developers can easily create RESTful APIs, web applications, and microservices. It is an open-source framework that can be used for building both web and mobile applications. Ktor can be used with Kotlin coroutines and flows to write asynchronous/reactive code in a more concise and readable style.

Ktor uses plugins to extend its functionality to the needs of the project. Some of them are included by default, others must be installed. In both cases, we must configure them to be able to use them.

We can create a Ktor project from the web generator: https://start.ktor.io/ (open the zip file) or from the IntellIjIdea plugin.

In our case, as we are experiencing numerous tennis tournaments, we are going to implement a reactive rest API to find out the most used rackets in a tennis tournament.

We must first create our project and add the following plugins: routing and Kotlin serialization. The first of them will allow us to create the routes or endpoints to manage the rackets. The second offers us the ability to exchange information in JSON.

We may need other plugins in the future, but it doesn’t matter, we’ll do it manually later.

In Adjust project settings, we select “configuration in HOCON file”.

Creating Ktor service using https://start.ktor.io/
Creating Ktor service using https://start.ktor.io/
Creating Ktor service with the IntellIj plugin
Creating Ktor service with the IntellIj plugin

Analyzing the initial code

Inital strctutre of the project

We have the following structure:

  • Application: It has the code that launches our service and where the plugins to configure are indicated.
  • plugins/Routing: Define the routing of our application based a
  • plugins/Serialization: Configure the serialization of our application based on JSON
  • resources/application.conf: Configure the application based on the environment variables.

Configure a Plugin

You can configure a plugin using extension functions. For example, configureRouting in Application.kt is an extension function that defines routing. This function is declared in a separate plugin package (the Routing.kt file).

fun Application.configureRouting() {
routing {
get("/") {
call.respondText("Hello World!")
}
}
}

Execute the Service

You can execute the main method, and then you can check your API on http://0.0.0.0:8080. You can see the famous message: “Hello World!”

Coding our service

Configure our service

The first step is to configure sour service and add some extra points to our initial application.conf to optimize development. This is the first version, and we will expand this file in subsequent tutorials.

Define our model

The first step is to define our model. In this case, it is Racket. We will do it inside a racket folder/package. It will have an id, brand, model, price, and number of tennis players who use it, image and the instant of creation and last modification. Also, we use a id constant to identify new or existing rackets.

data class Racket(
val id: Long = NEW_RACKET,
val brand: String,
val model: String,
val price: Double,
val numberTenisPlayers: Int = 0,
val image: String = DEFAULT_IMAGE,
val createdAt: LocalDateTime = LocalDateTime.now(),
val updatedAt: LocalDateTime = LocalDateTime.now(),
val isDeleted: Boolean = false
) {
companion object {
val NEW_RACKET = -1L
const val DEFAULT_IMAGE =
"https://upload.wikimedia.org/wikipedia/commons/thumb/3/3e/Tennis_Racket_and_Balls.jpg/800px-Tennis_Racket_and_Balls.jpg"
}
}

Developing our repository

The repository pattern is a design pattern that isolates the data layer from the rest of the app. The repository pattern has two purposes; first, it is an abstraction of the data layer, and second, it is a way of centralizing the handling of the domain objects. The idea is to have a generic abstract way for the app to work with the data layer without being bothered if the implementation is towards a local database, file, or a memory collection.

The methods are based on CRUD — Create, Read, Update, and Delete. In the first version, we used a map as a memory repository (in future parts we will use a reactive database). We use suspended functions, flows, and nullable types to perform the reactive and asynchronous use of the repository. We will use interfaces to be able to perform dependency injection later and comply with SOLID principles. We use Kotlin Logging to show the messages. Also, we use a new context and dispatcher to execute the methods in a special thread not to suspend the thread of the request petition.

class RacketsRepositoryImpl : RacketsRepository {
private val rackets = racketsDemoData()

override suspend fun findAll(): Flow<Racket> = withContext(Dispatchers.IO) {
logger.debug { "findAll" }

return@withContext rackets.values.toList().asFlow()
}

override suspend fun findById(id: Long): Racket? = withContext(Dispatchers.IO) {
logger.debug { "findById: $id" }

return@withContext rackets[id]
}

override suspend fun findAllPageable(page: Int, perPage: Int): Flow<Racket> = withContext(Dispatchers.IO) {
logger.debug { "findAllPageable: $page, $perPage" }


val myLimit = if (perPage > 100) 100L else perPage.toLong()
val myOffset = (page * perPage).toLong()

return@withContext rackets.values.toList().subList(myOffset.toInt(), myLimit.toInt()).asFlow()
}


override suspend fun findByBrand(brand: String): Flow<Racket> = withContext(Dispatchers.IO) {
logger.debug { "findByBrand: $brand" }
return@withContext rackets.values
.filter { it.brand.contains(brand, true) }
.asFlow()
}

override suspend fun save(entity: Racket): Racket = withContext(Dispatchers.IO) {
logger.debug { "save: $entity" }

if (entity.id == Racket.NEW_RACQUET) {
create(entity)
} else {
update(entity)
}
}

private fun update(entity: Racket): Racket {
logger.debug { "update: $entity" }

rackets[entity.id] = entity.copy(updatedAt = LocalDateTime.now())
return entity
}

private fun create(entity: Racket): Racket {
logger.debug { "create: $entity" }

val id = rackets.keys.maxOrNull()?.plus(1) ?: 1
val newEntity = entity.copy(id = id, createdAt = LocalDateTime.now(), updatedAt = LocalDateTime.now())
rackets[id] = newEntity
return newEntity
}

override suspend fun delete(entity: Racket): Racket? {
logger.debug { "delete: $entity" }

return rackets.remove(entity.id)
}

override suspend fun deleteAll() {
logger.debug { "deleteAll" }

rackets.clear()
}

override suspend fun saveAll(entities: Iterable<Racket>): Flow<Racket> {
logger.debug { "saveAll: $entities" }

entities.forEach { save(it) }
return entities.asFlow()
}
}

Setting our routes and endpoints

The next step is to define our routes for our endpoint; for this, we must know perfectly the HTTP verbs and status codes and the Request and Response using Ktor DSL. We can use this table:

Endpoits of our service
Endpoits of our service

We use an extension function called “racketsRoutes()” to define it, and use this function in our Routing plugging.

fun Application.configureRouting() {
routing {
get("/") {
call.respondText("Hello World!")
}
}

// Add our routes
racketsRoutes()
}

Remember, this is not the final version, and we need to improve many things, but this is a good starting point to check our service and how Ktor works.

// Define endpoint
private const val ENDPOINT = "api/rackets"

fun Application.racketsRoutes() {

// Repository
val rackets: RacketsRepository = RacketsRepositoryImpl()

// Define routing based on endpoint
routing {
route("/$ENDPOINT") {

// Get all rackets --> GET /api/racquets
get {
logger.info { "Get all rackets" }

// QueryParams ??
val page = call.request.queryParameters["page"]?.toIntOrNull()
val perPage = call.request.queryParameters["perPage"]?.toIntOrNull() ?: 10

if (page != null && page > 0) {
logger.debug { "GET ALL /$ENDPOINT?page=$page&perPage=$perPage" }

rackets.findAllPageable(page - 1, perPage).toList()
.run { call.respond(HttpStatusCode.OK, this) }

} else {
logger.debug { "GET ALL /$ENDPOINT" }

rackets.findAll().toList()
.run { call.respond(HttpStatusCode.OK, this) }
}
}

// Get one racket by id --> GET /api/rackets/{id}
get("{id}") {
logger.debug { "GET BY ID /$ENDPOINT/{id}" }

val id = call.parameters["id"]?.toLongOrNull()
id?.let {
rackets.findById(it)?.run { call.respond(HttpStatusCode.OK, this) }
?: call.respond(HttpStatusCode.NotFound, "Racket not found with ID $id")
} ?: call.respond(HttpStatusCode.BadRequest, "ID is not a number")
}

// Get one rackets by brand --> GET /api/rackets/brand/{brand}
get("brand/{brand}") {
logger.debug { "GET BY BRAND /$ENDPOINT/brand/{brand}" }

val brand = call.parameters["brand"]
brand?.let {
rackets.findByBrand(it).toList()
.run { call.respond(HttpStatusCode.OK, this) }
} ?: call.respond(HttpStatusCode.BadRequest, "Brand is not a string")
}

// Create a new racket --> POST /api/rackets
post {
logger.debug { "POST /$ENDPOINT" }

val racket = call.receive<Racket>()
rackets.save(racket)
.run { call.respond(HttpStatusCode.Created, this) }
}

// Update a racket by id --> PUT /api/rackets/{id}
put("{id}") {
logger.debug { "PUT /$ENDPOINT/{id}" }

val id = call.parameters["id"]?.toLongOrNull()
id?.let {
val racket = call.receive<Racket>()
// exists?
rackets.findById(it)?.let {
rackets.save(racket)
.run { call.respond(HttpStatusCode.OK, this) }
} ?: call.respond(HttpStatusCode.NotFound, "Racket not found with ID $id")
} ?: call.respond(HttpStatusCode.BadRequest, "ID is not a number")
}

// Delete a racket by id --> DELETE /api/racquets/{id}
delete("{id}") {
logger.debug { "DELETE /$ENDPOINT/{id}" }

val id = call.parameters["id"]?.toLongOrNull()
id?.let {
// exists?
rackets.findById(it)?.let { racquet ->
rackets.delete(racquet)
.run { call.respond(HttpStatusCode.NoContent) }
} ?: call.respond(HttpStatusCode.NotFound, "Racket not found with ID $id")
} ?: call.respond(HttpStatusCode.BadRequest, "ID is not a number")
}
}
}
}

Testing in Postman

Finally we can test our API rest in Postman or any REST Client. Postman is a famous client to test API REST. You can configure our request and test the responses.

Get all the rackets
Get all the rackets
Get a racket by ID
Post a new racket
Post a new racket
Update a racket with a ID
Update a racket with a ID
Delete a racket by a ID

Wrapping up

We started a series of exciting topics to capture the ideas of reactive programming to generate API REST.

Now we have a good starting point to begin working on. We have shown how Ktor works to develop a Reactive API REST, but we need to improve a lot of our code. But not everything can be done in the first step. Little by little, we will learn what we need in the following tutorials.

We have focused on creating our first endpoints and testing them on Postman. But we still have a lot to do in future tutorials.

  • Validate Requests.
  • Errors and Exceptions.
  • Railway Oriented Programming.
  • Using a Cache method.
  • Using a Reactive Database.
  • Upload files.
  • Using web sockets to implement real-time notifications.
  • Using Koin to inject our dependencies.
  • Security connections with SSL
  • Authentication and Authorization with JWT
  • Test our endpoints.

You have the code of this project in: https://github.com/joseluisgs/ktor-reactive-rest-hyperskill. The code of this part is this link: https://github.com/joseluisgs/ktor-reactive-rest-hyperskill/releases. Please don’t forget give a star or follow me to be aware of new tutorials and news.

You can follow it commit by commit and use the Postman backup file to test it.

In addition, in Hyperskill, You can deepen and learn all the concepts and more through different topics and tasks that will help you improve as a developer in Kotlin technologies.

The following tracks offered by JetBrains Academy on Hyperskill can be a perfect starting point. You will find all the information and explanation of concepts and techniques shown in these articles. Don’t miss them!

With these tracks, you will gain practical experience working with modern tools and learn how to develop server-side applications, keep the data in your databases persistent, and test the functionality of your apps using modern tools.

Let us know in the comments below if you have any questions or feedback regarding this blog. You can also follow us on social media to stay up-to-date with our latest articles and projects. We are on Reddit, LinkedIn and Facebook.

--

--

José Luis González Sánchez
Hyperskill

PhD. Software Development. Loving the art of teaching how to develop software. Kotlin Trainer Certified by JetBrains and Member of Hyperskill Kotlin team.