I tried Kotlin & KTor | My experience
Hello everyone! In this article, I will talk about my experience in building a simple REST API with Ktor and Kotlin.
Hello everyone!
In this article, I will talk about my experience in building a simple REST API with Ktor and Kotlin.
Let’s start!
Ktor as Spring Boot has a project generator, so you can easily create a project from its website
Let’s examine our pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>vrnsky.io</groupId>
<artifactId>food-delivery</artifactId>
<version>0.0.1</version>
<name>food-delivery</name>
<description>food-delivery</description>
<properties>
<ktor_version>2.3.12</ktor_version>
<kotlin.code.style>official</kotlin.code.style>
<kotlin_version>2.0.10</kotlin_version>
<logback_version>1.4.14</logback_version>
<slf4j_version>2.0.9</slf4j_version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<kotlin.compiler.incremental>true</kotlin.compiler.incremental>
<main.class>vrnsky.io.ApplicationKt</main.class>
</properties>
<repositories>
</repositories>
<dependencies>
<dependency>
<groupId>io.ktor</groupId>
<artifactId>ktor-server-core-jvm</artifactId>
<version>${ktor_version}</version>
</dependency>
<dependency>
<groupId>io.ktor</groupId>
<artifactId>ktor-server-content-negotiation-jvm</artifactId>
<version>${ktor_version}</version>
</dependency>
<dependency>
<groupId>io.ktor</groupId>
<artifactId>ktor-serialization-kotlinx-json-jvm</artifactId>
<version>${ktor_version}</version>
</dependency>
<dependency>
<groupId>io.ktor</groupId>
<artifactId>ktor-serialization-jackson-jvm</artifactId>
<version>${ktor_version}</version>
</dependency>
<dependency>
<groupId>io.ktor</groupId>
<artifactId>ktor-server-webjars-jvm</artifactId>
<version>${ktor_version}</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>io.ktor</groupId>
<artifactId>ktor-server-swagger-jvm</artifactId>
<version>${ktor_version}</version>
</dependency>
<dependency>
<groupId>io.ktor</groupId>
<artifactId>ktor-server-netty-jvm</artifactId>
<version>${ktor_version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback_version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j_version}</version>
</dependency>
<dependency>
<groupId>io.ktor</groupId>
<artifactId>ktor-server-test-host-jvm</artifactId>
<version>${ktor_version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test-junit</artifactId>
<version>${kotlin_version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-debug</artifactId>
<version>1.6.4</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<resources>
<resource>
<directory>${project.basedir}/src/main/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin_version}</version>
<configuration>
<jvmTarget>1.8</jvmTarget>
</configuration>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<executions>
<execution>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>${main.class}</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.6</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>${main.class}</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>assemble-all</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>2.0.10</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
<configuration>
<compilerPlugins>
<plugin>kotlinx-serialization</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-serialization</artifactId>
<version>${kotlin_version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
As you may see there are quite a lot of dependencies compared to Spring Boot projects. In the Spring Boot project things such as content negotiation and serialization works
out-of-box.
The analog of class annotated with SpringBootApplication annotation below:
package vrnsky.io
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import vrnsky.io.plugins.*
fun main() {
embeddedServer(Netty, port = 9090, host = "0.0.0.0", module = Application::module)
.start(wait = true)
}
fun Application.module() {
configureSerialization()
configureRouting()
}
So since the serialization and content negotiation not working out-of-box we have to configure it through the extension functions. These actions take place in two classes, which are located in the plugins package.
Routing.kt
package vrnsky.io.plugins
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.swagger.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.webjars.*
fun Application.configureRouting() {
install(Webjars) {
path = "/webjars" //defaults to /webjars
}
routing {
swaggerUI(path = "swagger", swaggerFile = "openapi/documentation.yaml")
get("/") {
call.respondText("Hello World!")
}
get("/webjars") {
call.respondText("<script src='/webjars/jquery/jquery.js'></script>", ContentType.Text.Html)
}
}
}
Serialization.kt
package vrnsky.io.plugins
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.*
import io.ktor.serialization.jackson.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.json.Json
fun Application.configureSerialization() {
install(ContentNegotiation) {
val json = Json {
ignoreUnknownKeys = true
}
json(
json
)
jackson {
enable(JsonGenerator.Feature.IGNORE_UNKNOWN)
enable(SerializationFeature.INDENT_OUTPUT)
}
}
routing {
get("/json/kotlinx-serialization") {
call.respond(mapOf("hello" to "world"))
}
get("/json/jackson") {
call.respond(mapOf("hello" to "world"))
}
}
}
Before going further we have to create OpenAPI documentation for our service to provide clients with detailed information about the service’s API. We already configured a service to provide it, but did not describe it.
swaggerUI(path = "swagger", swaggerFile = "openapi/documentation.yaml")
Let’s create file documentation.yml inside the src/main/resources/open folder.
openapi: "3.1.0"
info:
title: "Food Delivery Subscription API"
description: |
Provide CRUD operations on subscriptions to service
version: "1.0.0"
servers:
- url: "http://localhost:9090"
tags:
- name: subscription
description: "REST API for managing subscriptions"
paths:
/:
get:
description: ""
responses:
"200":
description: "OK"
content:
text/plain:
schema:
type: "string"
examples:
Example#1:
value: "Hello World!"
/subscription:
post:
tags:
- subscription
summary: "Create a new subscription"
description: "Create a new subscription"
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Subscription"
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/Subscription"
put:
tags:
- subscription
summary: "Update existing subscription"
description: "Update existing subscription"
parameters:
- in: path
name: id
required: true
schema:
type: string
description: "ID of subscription"
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/Subscription"
/subscription/{id}:
get:
tags:
- subscription
summary: "Get existing subscription by ID"
description: "Get existing subscription by ID"
parameters:
- in: path
name: id
required: true
schema:
type: string
description: "ID of subscription"
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/Subscription"
delete:
tags:
- subscription
summary: "Delete existing subscription"
description: "Delete existing subscription"
parameters:
- in: path
name: id
required: true
schema:
type: string
description: "ID of subscription"
/webjars:
get:
description: ""
responses:
"200":
description: "OK"
content:
text/html:
schema:
type: "string"
examples:
Example#1:
value: "<script src='/webjars/jquery/jquery.js'></script>"
/json/jackson:
get:
description: ""
responses:
"200":
description: "OK"
content:
'*/*':
schema:
$ref: "#/components/schemas/Map_String"
/json/kotlinx-serialization:
get:
description: ""
responses:
"200":
description: "OK"
content:
'*/*':
schema:
$ref: "#/components/schemas/Map_String"
components:
schemas:
Subscription:
description: Definition subscription
properties:
id: string
startDate:
type: date
format: date
endDate:
type: string
format: date
Map:
type: "object"
properties: {}
Map_String:
type: "string"
We can start our Ktor service and open Swagger UI
The next step is to describe our object model
package vrnsky.io.model
import kotlinx.serialization.Serializable
import vrnsky.io.LocalDateSerializer
import java.time.LocalDate
@Serializable
data class Subscription(
var id: String?,
@Serializable(with = LocalDateSerializer::class) val startDate: LocalDate,
@Serializable(with = LocalDateSerializer::class) val endDate: LocalDate,
)
As you may see the serialization of LocalDate is not supported out-of-box. So we have to implement it by ourselves.
package vrnsky.io
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializer
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.time.LocalDate
import java.time.format.DateTimeFormatter
@OptIn(ExperimentalSerializationApi::class)
@Serializer(forClass = LocalDate::class)
object LocalDateSerializer : KSerializer<LocalDate> {
private val formatter = DateTimeFormatter.ISO_LOCAL_DATE
override fun serialize(encoder: Encoder, value: LocalDate) {
encoder.encodeString(value.format(formatter))
}
override fun deserialize(decoder: Decoder): LocalDate {
return LocalDate.parse(decoder.decodeString(), formatter)
}
}
For the sake of simplicity, I will stick with a solution not to use any database and ORM at the moment. The service will persist data inside memory. Now it is time to create a service layer.
package vrnsky.io.service
import vrnsky.io.extension.withId
import vrnsky.io.model.Subscription
import java.util.UUID
class SubscriptionService(val subscriptions: MutableMap<String, Subscription>) {
fun createSubscription(subscription: Subscription): Subscription? {
val subscriptionWithId = subscription.withId(UUID.randomUUID().toString())
subscriptionWithId.id?.let { subscriptions.put(it, subscriptionWithId) }
return subscriptions[subscriptionWithId.id]
}
fun updateSubscription(subscription: Subscription): Subscription? {
subscription.id?.let { subscriptions.put(it, subscription) }
return subscriptions[subscription.id]
}
fun getSubscription(id: String?): Subscription? {
return subscriptions[id]
}
fun deleteSubscription(id: String?) {
subscriptions.remove(id)
}
}
One thing that I already liked while writing this article is the Kotlin extension function. They help you keep your data classes very small.
package vrnsky.io.extension
import vrnsky.io.model.Subscription
fun Subscription.withId(id: String): Subscription = Subscription (
id = id,
startDate = endDate,
endDate = endDate
)
The last step left — configure the routing of our application.
package vrnsky.io.plugins
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.swagger.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.webjars.*
import vrnsky.io.model.Subscription
import vrnsky.io.service.SubscriptionService
fun Application.configureRouting() {
val subscriptionService = SubscriptionService(mutableMapOf())
install(Webjars) {
path = "/webjars" //defaults to /webjars
}
routing {
swaggerUI(path = "swagger", swaggerFile = "openapi/documentation.yaml")
get("/") {
call.respondText("Hello World!")
}
get("/webjars") {
call.respondText("<script src='/webjars/jquery/jquery.js'></script>", ContentType.Text.Html)
}
post("/subscription") {
val subscription = call.receive<Subscription>()
call.respondNullable(
message = subscriptionService.createSubscription(subscription)
)
}
put("/subscription/") {
val subscription = call.receive<Subscription>()
call.respondNullable(
message = subscriptionService.updateSubscription(subscription))
}
get("/subscription/{id}") {
val subscriptionId = call.parameters["id"]
call.respondNullable(
message = subscriptionService.getSubscription(subscriptionId)
)
}
delete("/subscription/{id}") {
val subscriptionId = call.parameters["id"]
subscriptionService.deleteSubscription(subscriptionId)
call.respond(HttpStatusCode.NoContent)
}
}
}
Conclusion
Can I say that I liked Kotlin — Yes, can I say that I liked Ktor — partially.
For most of my job projects and pet projects, I was using Spring Boot and for me, there is no reason to move from Spring Boot to Ktor. If you have a good reason why Ktor is the appropriate choice let me know in the comments
Thank you,
Happy reading!