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@Serializableclasses to and from JSON automatically.- Each route handler is a suspend lambda, so it can call databases and other services without blocking a thread.
ConcurrentHashMapandAtomicIntegerkeep 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.