Skip to content
AgileLogics
7 min read

Back-End Development with Ktor

Prerequisites: Structured concurrency: async and scopes, Working with JSON

Ktor is JetBrains' framework for building web servers (and HTTP clients) in Kotlin. It's lightweight, built on coroutines, and configured with Kotlin DSLs. (Many companies also use Kotlin with Spring Boot, which fully supports Kotlin.)

Generate a project at start.ktor.io or with IntelliJ's Ktor project wizard, choosing the Routing, Content Negotiation, and kotlinx.serialization plugins.

A JSON API

import io.ktor.http.HttpStatusCode
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.request.receive
import io.ktor.server.response.respond
import io.ktor.server.routing.get
import io.ktor.server.routing.post
import io.ktor.server.routing.routing
import kotlinx.serialization.Serializable
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger

@Serializable
data class Task(val id: Int = 0, val title: String, val done: Boolean = false)

fun Application.module() {
    install(ContentNegotiation) { json() }

    val tasks = ConcurrentHashMap<Int, Task>()
    val nextId = AtomicInteger(1)

    routing {
        get("/health") {
            call.respond(mapOf("status" to "ok"))
        }
        get("/tasks") {
            call.respond(tasks.values.sortedBy { it.id })
        }
        post("/tasks") {
            val input = call.receive<Task>()
            if (input.title.isBlank()) {
                call.respond(HttpStatusCode.UnprocessableEntity, mapOf("error" to "title is required"))
                return@post
            }
            val task = input.copy(id = nextId.getAndIncrement())
            tasks[task.id] = task
            call.respond(HttpStatusCode.Created, task)
        }
    }
}

fun main() {
    embeddedServer(Netty, port = 8080, module = Application::module).start(wait = true)
}
$ curl -X POST localhost:8080/tasks -H "Content-Type: application/json" -d '{"title":"Learn Ktor"}'
{"id":1,"title":"Learn Ktor","done":false}
  • install(ContentNegotiation) { json() } converts @Serializable classes to and from JSON automatically.
  • Each route handler is a suspend lambda, so it can call databases and other services without blocking a thread.
  • ConcurrentHashMap and AtomicInteger keep the in-memory store safe when requests arrive at the same time. A real app would use a database.

Databases

Exposed (JetBrains' SQL library) and plain JDBC with a connection pool such as HikariCP are common. Always use parameterized queries — never build SQL by inserting user input into a string. Run blocking JDBC calls inside withContext(Dispatchers.IO).

Testing

Ktor's testApplication runs the app in memory, with no real server:

import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode
import io.ktor.server.testing.testApplication
import kotlin.test.Test
import kotlin.test.assertEquals

class HealthTest {
    @Test
    fun `health endpoint responds`() = testApplication {
        application { module() }
        val response = client.get("/health")
        assertEquals(HttpStatusCode.OK, response.status)
        assertEquals("""{"status":"ok"}""", response.bodyAsText())
    }
}

Production concerns

Ktor has plugins for authentication (JWT, sessions, OAuth), CORS, request logging (CallLogging), status pages for error handling, and rate limiting. The Deployment lesson covers packaging and running it.