iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Ktor (server)

Ktor is JetBrains async, multi-platform web framework: server engine + a typed HTTP client + plugin system. Built on coroutines, runs on Netty/CIO/Jetty, scales like a Node app would but with Kotlin types. Use it when you want a server you can also share types with from the Android client.

Ktor server with routing, validation, and auth

EXAMPLE
// build.gradle.kts
// dependencies {
//   implementation("io.ktor:ktor-server-core:2.3.12")
//   implementation("io.ktor:ktor-server-netty:2.3.12")
//   implementation("io.ktor:ktor-server-content-negotiation:2.3.12")
//   implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.12")
//   implementation("io.ktor:ktor-server-auth:2.3.12")
//   implementation("io.ktor:ktor-server-auth-jwt:2.3.12")
//   implementation("io.ktor:ktor-server-status-pages:2.3.12")
//   implementation("io.ktor:ktor-server-cors:2.3.12")
//   implementation("io.ktor:ktor-server-call-logging:2.3.12")
// }

import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.callloging.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable

@Serializable
data class CreateOrder(val customer: String, val totalCents: Long)

@Serializable
data class Order(val id: String, val customer: String, val totalCents: Long, val status: String)

@Serializable
data class ErrorBody(val code: String, val message: String)

fun main() {
    embeddedServer(Netty, port = 3000, host = "0.0.0.0") {
        install(ContentNegotiation) { json() }
        install(CallLogging)
        install(CORS) {
            allowHost("app.example.com", schemes = listOf("https"))
            allowCredentials = true
        }
        install(StatusPages) {
            exception<Throwable> { call, cause ->
                call.application.environment.log.error(cause.message, cause)
                call.respond(HttpStatusCode.InternalServerError, ErrorBody("internal", ""))
            }
        }

        install(Authentication) {
            jwt("auth-jwt") {
                realm = "shop"
                verifier(io.ktor.server.auth.jwt.makeJwkProvider("https://issuer.example.com"), "shop-api")
                validate { credential ->
                    if (credential.payload.audience.contains("shop-api")) JWTPrincipal(credential.payload) else null
                }
            }
        }

        routing {
            get("/healthz") { call.respondText("ok") }

            authenticate("auth-jwt") {
                route("/api/orders") {
                    val store = mutableMapOf<String, Order>()

                    post {
                        val body = call.receive<CreateOrder>()
                        if (body.customer.isBlank() || body.totalCents < 0) {
                            call.respond(HttpStatusCode.BadRequest, ErrorBody("validation", "bad fields"))
                            return@post
                        }
                        val id = java.util.UUID.randomUUID().toString()
                        val order = Order(id, body.customer, body.totalCents, "new")
                        store[id] = order
                        call.response.headers.append(HttpHeaders.Location, "/api/orders/$id")
                        call.respond(HttpStatusCode.Created, order)
                    }

                    get("/{id}") {
                        val id = call.parameters["id"] ?: return@get call.respond(HttpStatusCode.BadRequest)
                        val order = store[id] ?: return@get call.respond(HttpStatusCode.NotFound)
                        call.respond(order)
                    }
                }
            }
        }
    }.start(wait = true)
}

// ===== Typed client (Ktor client) for unit + integration tests =====
// suspend fun createOrder(client: HttpClient, body: CreateOrder): Order =
//     client.post("/api/orders") {
//         contentType(ContentType.Application.Json); setBody(body)
//     }.body()
//
// The SAME data classes are reused on Android (or another Ktor server),
// guaranteeing wire compatibility at compile time.

Why it matters

Share serializable data classes between your Ktor server and your Kotlin client/Android app. The wire format stops being a contract you maintain in two places — it is the same class, the compiler verifies both sides, and renaming a field is a refactor instead of a coordination event between two teams.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// build.gradle.kts: implementation("io.ktor:ktor-server-netty:…")
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.response.*

fun main() {
    embeddedServer(Netty, port = 8080) {
        routing { get("/") { call.respondText("hello") } }
    }.start(wait = true)
}
Try it Yourself »

Discussion

Loading…