Welcome Ktor Client — Your next Http client for Kotlin based Project. Part I.

Java Jedi
8 min readSep 6, 2024

--

Kodee sending his regards 😎

As a software developer, it is inevitable that you develop applications interacting with external services nowadays. If you are a Java or Kotlin Backend Developer, there are a lot of various HTTP client libraries, such as RestTemplate, WebClient, Feign, etc. that you might have encountered during your career. As Kotlin continues to evolve and its popularity is growing, maybe it is time to get a closer look at the Kotlin based HTTP client.

In this post, I will shed light on what makes Ktor-client so appealing to use. I will highlight its main features with practical examples and discuss why you might want to switch from alternative HTTP clients to Ktor-client.

Before we go further, it is worth mentioning Ktor-client is appealing to use for Kotlin based projects or if you are considering to use Kotlin for your next projects with much service interaction over HTTP in mind.

Ktor-client stands out from other HTTP clients by providing the following features:

  • Kotlin-First Design — Coroutines and Non-blocking IO
  • Lightweight and Modern
  • Flexibility and Extensibility
  • Kotlin DSL

These are the features that make Ktor stand out from the other libraries and we will discuss one by one.

Kotlin-First Design — Coroutines and Non-blocking IO

Being developed on top Kotlin only, Ktor-client makes use one of the main features of the language — coroutines. It enables implementing more scalable, asynchronous and high-performant calls to external services rather than making blocking ones since all the functions for provided by the Ktor are suspending functions. Therefore, this enables the application to handle more requests concurrently.

Flexibility and Extensibility

Ktor’s plugin-based architecture makes it highly flexible and extensible. With Ktor you specify only the features that you need in your application. It is similar to building blocks of some construction. There are a lot of plugins, let’s say building blocks, and you are free to building the construction of your desire. For example, you can configure — Serialization, Logging, even the engine depending on whether to use HTTP 1.1 or HTTP 2.

Lightweight and Modern

Thanks to the plugin based architecture, the Ktor-client does not pull all the dependencies instantly. It allows you to specify the ones you need. Besides, Ktor-client does not rely on heavy abstractions provided using annotations in the libraries like RestTemplate and Feign. As a result, the overall memory footprint is smaller since you have control over the dependencies you use in the project.

Kotlin DSL

Ktor’s modern architecture embraces Kotlin DSL that allows to write a concise, clean and more readable code for building requests. This DSL provides a concise and flexible API to configure a client and thus making the overall code more readable and maintainable.

Practical example

Let’s implement a simple example with Ktor-client and configure it to have logging and serialization. It will be a small gradle based project.

Initialize a file — build.gradle.kts.

val ktor_version = "2.3.12"

plugins {
kotlin("jvm") version "1.9.22"
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.10"
}

group = "com.atomic-coding"
version = "1.0-SNAPSHOT"

repositories {
mavenCentral()
}


dependencies {
implementation("io.ktor:ktor-client-core:$ktor_version")
// For JVM
implementation("io.ktor:ktor-client-cio-jvm:$ktor_version")

// For Kotlin Multiplatform
// implementation("io.ktor:ktor-client-cio:$ktor_version")
testImplementation("org.jetbrains.kotlin:kotlin-test")
}

tasks.test {
useJUnitPlatform()
}
kotlin {
jvmToolchain(19)
}

Ktor-client comes with two core dependencies:

implementation("io.ktor:ktor-client-core:$ktor_version")
implementation("io.ktor:ktor-client-cio-jvm:$ktor_version")

// implementation("io.ktor:ktor-client-cio:$ktor_version")

The client will not work without these two:

  • io.ktor:ktor-client-core — responsible for providing the following components and functionality — making HTTP requests (GET, POST, PUT, DELETE, etc), Serialization, Deserialization, etc
  • io.ktor:ktor-client-cio-jvm / io.ktor:ktor-client-cio — responsible for adding the http engine to the Ktor-client. ktor-client-cio-jvm is tailored and optimized for JVM based projects, whereas ktor-client-cio is used for Kotlin multiplatform projects. It is important to choose wisely depending on the project you build to avoid and reduce complexity and additional overhead brought by incorrect usage of dependency.

Since we are building a JVM based project, lets re-write the dependencies block:

dependencies {
implementation("io.ktor:ktor-client-core:$ktor_version")
implementation("io.ktor:ktor-client-cio-jvm:$ktor_version")

testImplementation("org.jetbrains.kotlin:kotlin-test")
}

With these dependencies, we can configure our client and make Http call. For this demo, I will use {JSON} Placeholder to send our requests.

Demo:

Output. For brevity i omitted the whole response as it is large

In the output you can see the warning in red — it shows that logging is not configured. As I mentioned before, Ktor comes with basic functionality and it is up to you whether to include the desired functionality as logging, serialization, etc. Let’s configure Logging next.

Logging

To configure logging, we need to add a logging plugin and further configure our client. First, we need to add the required dependency to dependencies block in the build.gradle.kts.

implementation("io.ktor:ktor-client-logging:$ktor_version")

The dependencies block will be as following:

dependencies {
// CORE LIBRARIES
implementation("io.ktor:ktor-client-core:$ktor_version")
implementation("io.ktor:ktor-client-cio-jvm:$ktor_version")

// Logging
implementation("io.ktor:ktor-client-logging:$ktor_version")
}

To configure logging — we specify Logging in the install() block. The level property may have one of the following values:
INFO
ALL
BODY
NONE
HEADERS

In our example, we will use LogLevel.INFO

fun httpClient() = HttpClient(CIO) {
install(Logging) {
level = LogLevel.INFO
}
}
Demo
Output

Ooops! We encountered the same warning again although we configured the Logging plugin. This is because our client needs the logging library as well, such as — Logback, Log4j, Log4j2. Since we have not included any of these logging libraries in our project, we encountered this warning. Let’s add the logging library — in this example, I want to use Log4j2 as I find it to be most latest and most performant logging library.

Log4j2 dependencies:

val log4j2_version = "2.23.1"

implementation("org.apache.logging.log4j:log4j-core:$log4j2_version")
implementation("org.apache.logging.log4j:log4j-api:$log4j2_version")
implementation("org.apache.logging.log4j:log4j-slf4j-impl:$log4j2_version")

To finish logging configuration is to add the log4j2.xml file as last piece of the puzzle. Add the log4j2.xml file in the resources folder.

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
<File name="File" fileName="logs/app.log">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>
</File>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="File"/>
</Root>
</Loggers>
</Configuration>

With this set up, lets run the same program

Demo

Let’s look the output we get. As we see, we get all the logs related to the request of LogLevel.INFO.

Output

ContentNegotiation, aka Serialization / Deserialization.

In the example above we just print the response we get as text. This is unlikely what we do while developing applications, we usually serialize the response to the objects we need. Let’s deserialize the response to the object we want. From the JSON response, we know the number of fields and their types. Let’s create our Kotlin class to deserialize response into.

data class Post(
val id: Int,
val userId: Int,
val title: String,
val body: String
)

To deserialize reponse into an object of specific class, we use method — body<T>(), where T — a specific class. For example, in our program we cast our response into a list of posts — List<Post>.
val posts = response.body<List<Post>>()
Let’ run the program and see the result.

Demo
Output

Ooops! We bumpted into another issue — io.ktor.client.call.NoTransformationFoundException. This exception says that content negotiation has not been configured. To solve this issue, we need to install additional plugins — ContentNegotiation and Serialization / Deserialization:

implementation("io.ktor:ktor-client-content-negotiation:$ktor_version")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")

The build.gralde.kts file will be have the following content

ktor-client-content-negotiation responsible for negotiating different content types betwen a client and server, such as XML, JSON or Protobuf.

ktor-serialization-kotlinx-json responsible for converting data in JSON format into Kotlin objects and vice versa.

val ktor_version = "2.3.12"
val log4j2_version = "2.23.1"

plugins {
kotlin("jvm") version "1.9.22"
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.10"
}

group = "com.atomic-coding"
version = "1.0-SNAPSHOT"

repositories {
mavenCentral()
}


dependencies {
// CORE LIBRARIES
implementation("io.ktor:ktor-client-core:$ktor_version")
implementation("io.ktor:ktor-client-cio-jvm:$ktor_version")

// Logging Plugins
implementation("io.ktor:ktor-client-logging:$ktor_version")

// FOR JSON AND Serialization Plugins
implementation("io.ktor:ktor-client-content-negotiation:$ktor_version")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")

implementation("org.apache.logging.log4j:log4j-core:$log4j2_version")
implementation("org.apache.logging.log4j:log4j-api:$log4j2_version")
implementation("org.apache.logging.log4j:log4j-slf4j-impl:$log4j2_version")

testImplementation("org.jetbrains.kotlin:kotlin-test")
}

tasks.test {
useJUnitPlatform()
}
kotlin {
jvmToolchain(19)
}

We also need to annotate Post class with @Serializable annotation. This annotation is used with Kotlin data classes to indicate that these objects can be serialized and deserialized.

import kotlinx.serialization.Serializable

@Serializable
data class Post(
val id: Int,
val userId: Int,
val title: String,
val body: String
)

Addititionally, we need to configure our client code. Since the server response is in JSON format, we need to specify that Json is used to deserialize response into Kotlin class.

fun httpClient() = HttpClient(CIO) {
install(Logging) {
level = LogLevel.INFO
}
install(ContentNegotiation){
json(Json { ignoreUnknownKeys=true; prettyPrint = true })
}
}

Let’s run the program and see the output.

Demo

As we can see, everything is working as expected.

Output

Let’s play with the posts and apply some operations.

Exercise: take the first 10 posts and print their titles.

Solution

Output:

Output

In summary, Ktor-client offers a powerful and flexible alternative to traditional HTTP clients like RestTemplate and Feign in the Kotlin / Java world. So far we have discussed various aspects of Ktor and implemented a demo example. In the next post, I will highlight other features of Ktor-client and show more examples.

If you have any questions or experiences to share, feel free to drop a comment below. I will be glad to read. And don’t forget to drop a visit to Ktor’s official documentation. Happy Coding and see you in the next post.

--

--

Java Jedi
Java Jedi

No responses yet