Skip to main content

Kotlin Ktor Framework

Introduction

Ktor is a lightweight, modern framework for building asynchronous server-side applications in Kotlin. Developed by JetBrains, the creators of Kotlin, Ktor leverages Kotlin's language features like coroutines, creating a highly efficient and intuitive API for developing web applications. Unlike traditional Java-based frameworks, Ktor was designed from the ground up to be completely Kotlin-native, offering a more streamlined development experience.

Whether you're building a REST API, websocket service, or a full-stack web application, Ktor provides the tools and flexibility needed to create robust backend solutions with minimal boilerplate code. This guide will walk you through the basics of Ktor and help you get started with building your first Ktor application.

Getting Started with Ktor

Setting Up a Ktor Project

The easiest way to start a new Ktor project is by using the Ktor Project Generator provided by JetBrains. Alternatively, you can set up a Gradle project manually.

Here's how you can set up a basic Ktor project using Gradle:

kotlin
// build.gradle.kts

plugins {
kotlin("jvm") version "1.8.20"
id("io.ktor.plugin") version "2.3.0"
}

group = "com.example"
version = "0.0.1"

application {
mainClass.set("com.example.ApplicationKt")
}

repositories {
mavenCentral()
}

dependencies {
implementation("io.ktor:ktor-server-core:2.3.0")
implementation("io.ktor:ktor-server-netty:2.3.0")
implementation("ch.qos.logback:logback-classic:1.4.7")

testImplementation("io.ktor:ktor-server-test-host:2.3.0")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:1.8.20")
}

Creating Your First Ktor Application

Now let's create a basic Ktor application that responds to HTTP requests:

kotlin
// src/main/kotlin/com/example/Application.kt

package com.example

import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun main() {
embeddedServer(Netty, port = 8080) {
routing {
get("/") {
call.respondText("Hello, World!")
}
}
}.start(wait = true)
}

When you run this application and navigate to http://localhost:8080 in your browser, you'll see the text "Hello, World!" displayed.

Core Concepts in Ktor

Application Structure

A Ktor application is built around several key concepts:

  1. Engine - Handles connections and dispatches requests (e.g., Netty, Tomcat)
  2. Application - The core container that holds all routes, plugins, and configurations
  3. Routing - Defines how different HTTP requests are handled
  4. Modules - Logical units containing configuration and features
  5. Plugins - Add functionality to your application (authentication, serialization, etc.)

Routing

Routing is one of the most important concepts in Ktor. It defines how your application responds to different HTTP requests:

kotlin
routing {
// Handle GET requests to the root path
get("/") {
call.respondText("Hello, World!")
}

// Handle GET requests to /users path
get("/users") {
call.respondText("List of users")
}

// Handle POST requests to /users path
post("/users") {
val userData = call.receiveText()
call.respondText("Created user from: $userData")
}

// Path parameters
get("/users/{id}") {
val id = call.parameters["id"]
call.respondText("User details for ID: $id")
}
}

Using Plugins

Plugins are a powerful feature in Ktor that allow you to enhance your application with additional functionality:

kotlin
fun main() {
embeddedServer(Netty, port = 8080) {
// Install ContentNegotiation plugin for JSON processing
install(ContentNegotiation) {
json()
}

// Install CallLogging plugin for request logging
install(CallLogging) {
level = Level.INFO
}

routing {
get("/") {
call.respondText("Hello, World!")
}
}
}.start(wait = true)
}

Make sure to add the necessary dependencies for the plugins:

kotlin
// For ContentNegotiation with JSON
implementation("io.ktor:ktor-server-content-negotiation:2.3.0")
implementation("io.ktor:ktor-serialization-jackson:2.3.0")
// or
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.0")

// For CallLogging
implementation("io.ktor:ktor-server-call-logging:2.3.0")

Building a RESTful API

Let's build a simple RESTful API for managing a collection of books:

kotlin
package com.example

import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

@Serializable
data class Book(val id: Int, val title: String, val author: String, val year: Int)

val books = mutableListOf(
Book(1, "Kotlin in Action", "Dmitry Jemerov", 2017),
Book(2, "Effective Kotlin", "Marcin Moskala", 2019),
Book(3, "Atomic Kotlin", "Bruce Eckel", 2021)
)

fun main() {
embeddedServer(Netty, port = 8080) {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
})
}

routing {
// Get all books
get("/books") {
call.respond(books)
}

// Get a specific book
get("/books/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, "Invalid ID format")
return@get
}

val book = books.find { it.id == id }
if (book != null) {
call.respond(book)
} else {
call.respond(HttpStatusCode.NotFound, "Book not found")
}
}

// Create a new book
post("/books") {
val newBook = call.receive<Book>()
books.add(newBook)
call.respond(HttpStatusCode.Created, newBook)
}

// Update an existing book
put("/books/{id}") {
val id = call.parameters["id"]?.toIntOrNull() ?: return@put call.respond(
HttpStatusCode.BadRequest, "Invalid ID format")

val updatedBook = call.receive<Book>()
val index = books.indexOfFirst { it.id == id }

if (index == -1) {
call.respond(HttpStatusCode.NotFound, "Book not found")
} else {
books[index] = updatedBook
call.respond(updatedBook)
}
}

// Delete a book
delete("/books/{id}") {
val id = call.parameters["id"]?.toIntOrNull() ?: return@delete call.respond(
HttpStatusCode.BadRequest, "Invalid ID format")

val removed = books.removeIf { it.id == id }
if (removed) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound, "Book not found")
}
}
}
}.start(wait = true)
}

To use this code, make sure to add the kotlinx serialization dependencies:

kotlin
plugins {
kotlin("jvm") version "1.8.20"
id("io.ktor.plugin") version "2.3.0"
kotlin("plugin.serialization") version "1.8.20" // Add this line
}

dependencies {
// Other dependencies
implementation("io.ktor:ktor-server-content-negotiation:2.3.0")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.0")
}

Authentication and Authorization

Securing your Ktor application is essential for most real-world applications. Ktor provides built-in support for different authentication methods:

kotlin
fun Application.configureSecurity() {
install(Authentication) {
basic("basic-auth") {
realm = "Ktor Server"
validate { credentials ->
if (credentials.name == "admin" && credentials.password == "password") {
UserIdPrincipal(credentials.name)
} else {
null
}
}
}

jwt("jwt-auth") {
realm = "Ktor Server"
verifier(
JWT.require(Algorithm.HMAC256("secret"))
.withAudience("jwt-audience")
.withIssuer("jwt-issuer")
.build()
)
validate { credential ->
if (credential.payload.audience.contains("jwt-audience")) {
JWTPrincipal(credential.payload)
} else {
null
}
}
}
}

routing {
authenticate("basic-auth") {
get("/protected/basic") {
val principal = call.principal<UserIdPrincipal>()
call.respondText("Hello, ${principal?.name}!")
}
}

authenticate("jwt-auth") {
get("/protected/jwt") {
val principal = call.principal<JWTPrincipal>()
val username = principal?.payload?.getClaim("username")?.asString()
call.respondText("Hello, $username!")
}
}
}
}

Add these dependencies for authentication:

kotlin
implementation("io.ktor:ktor-server-auth:2.3.0")
implementation("io.ktor:ktor-server-auth-jwt:2.3.0")

Testing in Ktor

Ktor provides a simple way to test your application using the testApplication function:

kotlin
class ApplicationTest {
@Test
fun testRoot() = testApplication {
application {
routing {
get("/") {
call.respondText("Hello, World!")
}
}
}

val response = client.get("/")
assertEquals(HttpStatusCode.OK, response.status)
assertEquals("Hello, World!", response.bodyAsText())
}

@Test
fun testGetBooks() = testApplication {
application {
module() // Your application's main module
}

val response = client.get("/books")
assertEquals(HttpStatusCode.OK, response.status)
// Further assertions on the response body
}
}

Structuring a Larger Ktor Application

As your application grows, it's important to structure it properly. Here's a recommended approach:

kotlin
// Application.kt
fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

fun Application.module() {
configureSerialization()
configureMonitoring()
configureSecurity()
configureRouting()
}

// Plugins.kt
fun Application.configureSerialization() {
install(ContentNegotiation) {
json()
}
}

fun Application.configureMonitoring() {
install(CallLogging)
}

fun Application.configureSecurity() {
// Authentication configuration
}

// Routing.kt
fun Application.configureRouting() {
routing {
bookRoutes()
userRoutes()
}
}

// BookRoutes.kt
fun Route.bookRoutes() {
route("/books") {
get {
// Get all books
}

get("/{id}") {
// Get a specific book
}

// Other book routes
}
}

// UserRoutes.kt
fun Route.userRoutes() {
route("/users") {
// User related routes
}
}

Deploying a Ktor Application

Ktor applications can be deployed in various ways:

  1. Fat JAR: Package your application as a self-contained JAR file

    kotlin
    // build.gradle.kts

    application {
    mainClass.set("com.example.ApplicationKt")
    }

    tasks {
    val fatJar = register<Jar>("fatJar") {
    dependsOn.addAll(listOf("compileJava", "compileKotlin", "processResources"))
    archiveClassifier.set("standalone")
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    manifest { attributes(mapOf("Main-Class" to application.mainClass)) }
    val sourcesMain = sourceSets.main.get()
    val contents = configurations.runtimeClasspath.get()
    .map { if (it.isDirectory) it else zipTree(it) } +
    sourcesMain.output
    from(contents)
    }
    build {
    dependsOn(fatJar)
    }
    }
  2. Docker: Create a Docker container for your application

    dockerfile
    FROM openjdk:11
    EXPOSE 8080:8080
    RUN mkdir /app
    COPY ./build/libs/*-standalone.jar /app/application.jar
    ENTRYPOINT ["java", "-jar", "/app/application.jar"]

Summary

In this guide, we've explored the Kotlin Ktor framework, a powerful tool for building backend applications with Kotlin. We've covered:

  • Setting up a basic Ktor project
  • Creating routes and handling HTTP requests
  • Using plugins to extend functionality
  • Building a complete RESTful API
  • Adding authentication and security
  • Testing Ktor applications
  • Structuring larger applications
  • Deployment options

Ktor's lightweight, flexible architecture and seamless integration with Kotlin's powerful features like coroutines make it an excellent choice for modern server-side development. Its non-invasive design allows developers to build applications exactly as they want, without forcing them into rigid patterns.

Additional Resources

  1. Official Ktor Documentation
  2. Ktor GitHub Repository
  3. Ktor Project Generator
  4. Kotlin Coroutines Guide

Exercises

  1. Create a simple todo list API with endpoints to create, read, update, and delete tasks.
  2. Add JWT authentication to your API and restrict access to certain endpoints.
  3. Implement a WebSocket endpoint that broadcasts messages to all connected clients.
  4. Create a Ktor client that consumes an external API and processes the results.
  5. Extend the book API example to include categories and searching functionality.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)