Skip to content

What it is

Ktor is JetBrains' asynchronous framework for HTTP servers and clients in Kotlin, built on coroutines with a plugin-based architecture.

Install plugins for content negotiation, authentication, logging and CORS, then declare routes in a nested DSL. Handlers are suspending functions.

Installation

implementation("io.ktor:ktor-server-netty:3.0.0")

Getting started

The smallest useful thing you can do with it, and what each part means.

Server with routing and serialisation
fun main() {
    embeddedServer(Netty, port = 8080) {
        install(ContentNegotiation) { json() }
        install(StatusPages) {
            exception<NotFoundException> { call, _ ->
                call.respond(HttpStatusCode.NotFound, ErrorResponse("not found"))
            }
        }

        routing {
            route("/books") {
                get("{id}") {
                    val id = call.parameters["id"]?.toIntOrNull()
                        ?: throw BadRequestException("id must be a number")
                    call.respond(store.find(id) ?: throw NotFoundException())
                }
                post {
                    val body = call.receive<CreateBook>()
                    call.respond(HttpStatusCode.Created, store.create(body))
                }
            }
        }
    }.start(wait = true)
}
StatusPages centralises error mapping, so handlers can throw domain exceptions rather than each constructing its own error response.
The HTTP client
val client = HttpClient(CIO) {
    install(ContentNegotiation) { json() }
    install(HttpTimeout) { requestTimeoutMillis = 10_000 }
    install(HttpRequestRetry) { retryOnServerErrors(maxRetries = 3); exponentialDelay() }
}

val book: Book = client.get("https://api.example.com/books/42").body()

client.close()   // or use it as a singleton for the app lifetime
The same plugin model applies to the client. It works on Android and Kotlin/Native too, which is why Multiplatform projects use it over OkHttp.

Advanced usage

Where the library earns its place over a simpler alternative.

Authentication and typed routes
install(Authentication) {
    jwt("auth-jwt") {
        verifier(jwkProvider, issuer)
        validate { credential ->
            credential.payload.getClaim("sub").asString()
                ?.let { JWTPrincipal(credential.payload) }
        }
        challenge { _, _ -> call.respond(HttpStatusCode.Unauthorized) }
    }
}

routing {
    authenticate("auth-jwt") {
        get("/me") {
            val principal = call.principal<JWTPrincipal>()!!
            call.respond(principal.payload.getClaim("sub").asString())
        }
    }
}
The authenticate block wraps a whole section of routes, so a new endpoint added inside it is protected by default rather than by remembering to annotate it.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

Cannot transform this request's content
ContentNegotiation is not installed, or the request lacks a Content-Type header the serialiser recognises.
Routes return 404 unexpectedly
Route nesting places the path under a parent prefix. Check the full concatenated path.

Best practices

  • Use StatusPages for centralised error handling instead of per-route responses.
  • Create one HttpClient and reuse it; constructing one per call is expensive.
  • Group protected routes inside authenticate blocks so security is the default.
  • Prefer Ktor for Multiplatform and Kotlin-first projects; Spring Boot has the larger ecosystem on the JVM.

Background

Why it exists, and what it was reacting to.

Ktor was designed to be Kotlin-first rather than a wrapper over a Java framework — routing is a DSL, everything is suspending, and it runs on JVM, Android and Kotlin/Native.