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.

I tried Kotlin & KTor | My experience
Swagger UI

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!

References

  1. Ktor Project Generator
  2. Content negotiation
  3. Serialization
  4. Ktor Serialization and Content Negotiation
  5. Kotlin Extension Function

Subscribe to Egor Voronianskii | Java Development and whatsoever

Sign up now to get access to the library of members-only issues.
Jamie Larson
Subscribe