← All shorts
46Ktor 2.3+2 min read

Ktor Client as the Shared Networking Layer

Ktor's multiplatform HTTP client puts request building, serialization, and error handling in one shared networking layer.

Base URL, headers, JSON parsing, retry. Every mobile app needs them, and most apps build them twice: Retrofit/OkHttp on Android, URLSession on iOS. Ktor's multiplatform client puts all of that in commonMain and swaps the HTTP engine per target underneath.

#The old way

OrdersApi on Android was a Retrofit interface with Moshi converters. On iOS it was a URLSession wrapper hand-parsing JSON with JSONDecoder. A new endpoint, a new header, a new error code — each one meant editing both, in two languages, against two serialization libraries.

#The new way

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

class OrdersApi(baseUrl: String) {
    private val client = HttpClient {
        install(ContentNegotiation) {
            json(Json { ignoreUnknownKeys = true })
        }
        defaultRequest {
            url(baseUrl)
        }
    }

    suspend fun fetchOrder(orderId: String): Order =
        client.get("orders/$orderId").body()

    suspend fun createOrder(order: Order): Order =
        client.post("orders") {
            contentType(ContentType.Application.Json)
            setBody(order)
        }.body()
}

commonMain depends on ktor-client-core, ktor-client-content-negotiation, and ktor-serialization-kotlinx-json. androidMain adds ktor-client-okhttp; iosMain adds ktor-client-darwin. HttpClient() picks up whichever engine is on the classpath for that target. No expect/actual needed for the client itself.

#Why it matters

  • One @Serializable model and one request/response shape instead of two, in sync by construction
  • Error mapping, auth headers, and retry policy live in one HttpClient { } block, not duplicated
  • Swapping engines (OkHttp for CIO, Darwin for a custom NSURLSession config) is a dependency change, not a rewrite

#Gotcha

HttpClient() with no explicit engine argument only resolves at compile time if exactly one engine artifact sits on that source set's classpath. Forget ktor-client-darwin in iosMain and the module fails to compile on the Native targets. Worse: if another engine leaks in transitively, it silently picks the wrong one. No runtime warning points at the missing dependency. You just get a client that behaves differently than expected on that platform.

kmpkotlincross-platform

Related shorts