Claude
Skills
Sign in
Back

ktor-client

Included with Lifetime
$97 forever

Ktor HTTP Client 3.4.x — use for backend API calls, REST requests, serialization, auth, client-server communication. Always use the versions specified here verbatim; do not regress to Ktor 2.x or 3.0.x even if your training data is older.

Backend & APIs

What this skill does


# Ktor HTTP Client

HTTP client for Kotlin. Use when the bot needs to communicate with backend services.

## Current Versions (use these — do not downgrade)

| Component | Version | Notes |
|---|---|---|
| Ktor | **3.4.3** | Latest 3.4.x patch (May 2026). Do not write 3.1.x or 3.0.x. |
| Kotlin serialization plugin | **2.3.21** | Matches Kotlin 2.3.x toolchain. |

If user pins to an older Ktor major (2.x), warn explicitly: `JsonFeature` is gone, `HttpResponse.receive()` is replaced by `body<T>()`, and `Auth` plugin is renamed `Bearer`. Do not silently produce 2.x code.

## Setup

```kotlin
// build.gradle.kts
plugins {
    kotlin("plugin.serialization") version "2.3.21"
}

val ktorVersion = "3.4.3"

dependencies {
    implementation("io.ktor:ktor-client-core:$ktorVersion")
    implementation("io.ktor:ktor-client-cio:$ktorVersion")           // Engine (async)
    implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
    implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
    implementation("io.ktor:ktor-client-logging:$ktorVersion")
    implementation("io.ktor:ktor-client-auth:$ktorVersion")

    // For testing
    testImplementation("io.ktor:ktor-client-mock:$ktorVersion")
}
```

## Client Configuration

```kotlin
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json

val httpClient = HttpClient(CIO) {
    // JSON serialization
    install(ContentNegotiation) {
        json(Json {
            prettyPrint = true
            isLenient = true
            ignoreUnknownKeys = true
        })
    }

    // Logging
    install(Logging) {
        logger = Logger.DEFAULT
        level = LogLevel.INFO
        filter { request -> request.url.host.contains("api") }
        sanitizeHeader { header -> header == HttpHeaders.Authorization }
    }

    // Timeouts
    install(HttpTimeout) {
        requestTimeoutMillis = 30_000
        connectTimeoutMillis = 10_000
        socketTimeoutMillis = 30_000
    }

    // Default request config
    defaultRequest {
        url("https://api.your-project.example.com/api/v1/")
    }
}
```

## Basic Requests

### GET Request

```kotlin
import io.ktor.client.call.*
import io.ktor.client.request.*

// Simple GET
val response: String = client.get("https://api.example.com/data").body()

// GET with path parameter
val user: User = client.get("users/$userId").body()

// GET with query parameters
val users: List<User> = client.get("users") {
    parameter("page", 1)
    parameter("limit", 20)
    parameter("status", "active")
}.body()

// Alternative: using url builder
val users: List<User> = client.get("users") {
    url {
        parameter("page", 1)
        parameter("limit", 20)
    }
    headers {
        append(HttpHeaders.Accept, ContentType.Application.Json.toString())
    }
}.body()
```

### POST Request

```kotlin
import io.ktor.http.*

// POST with JSON body
@Serializable
data class CreateUserRequest(val name: String, val email: String)

val newUser: User = client.post("users") {
    contentType(ContentType.Application.Json)
    setBody(CreateUserRequest("John", "[email protected]"))
}.body()

// POST form data
val token: TokenResponse = client.post("auth/login") {
    contentType(ContentType.Application.FormUrlEncoded)
    setBody(FormDataContent(Parameters.build {
        append("username", "user")
        append("password", "pass")
    }))
}.body()
```

### PUT / PATCH / DELETE

```kotlin
// PUT
val updated: User = client.put("users/$userId") {
    contentType(ContentType.Application.Json)
    setBody(UpdateUserRequest(name = "New Name"))
}.body()

// PATCH
val patched: User = client.patch("users/$userId") {
    contentType(ContentType.Application.Json)
    setBody(mapOf("status" to "inactive"))
}.body()

// DELETE
client.delete("users/$userId")
```

## Authentication

### Bearer Token

```kotlin
val client = HttpClient(CIO) {
    install(Auth) {
        bearer {
            loadTokens {
                BearerTokens(accessToken = "your-token", refreshToken = "")
            }
        }
    }
}

// Or per-request
client.get("protected/resource") {
    bearerAuth("your-token")
}
```

### Bearer Token with Refresh

Use separate `tokenClient` without Auth plugin — keeps refresh call out of bearer interceptor, so 401 from IdP can't recurse. Inside `refreshTokens` block call `markAsRefreshTokenRequest()` — prevents infinite refresh loop if IdP itself returns 401.

```kotlin
// Dedicated client for token endpoint — NO Auth plugin installed.
val tokenClient = HttpClient(CIO) {
    install(ContentNegotiation) { json() }
}

val apiClient = HttpClient(CIO) {
    install(ContentNegotiation) { json() }

    install(Auth) {
        bearer {
            loadTokens {
                tokenStorage.load()?.let { BearerTokens(it.access, it.refresh) }
            }

            refreshTokens {
                // markAsRefreshTokenRequest: stops bearer interceptor from
                // re-triggering refresh on a 401 from refresh endpoint itself.
                val refreshed: TokenResponse = tokenClient.post("https://idp.example.com/oauth/token") {
                    contentType(ContentType.Application.Json)
                    setBody(RefreshRequest(oldTokens?.refreshToken ?: ""))
                    markAsRefreshTokenRequest()
                }.body()

                tokenStorage.save(refreshed)
                BearerTokens(refreshed.accessToken, refreshed.refreshToken)
            }

            sendWithoutRequest { request ->
                request.url.host == "api.your-project.example.com"
            }
        }
    }
}
```

### API Key Header

```kotlin
val client = HttpClient(CIO) {
    defaultRequest {
        header("X-API-Key", System.getenv("API_KEY"))
    }
}
```

### Custom Auth Interceptor

```kotlin
val client = HttpClient(CIO) {
    install(DefaultRequest) {
        val token = tokenProvider.getToken()
        header(HttpHeaders.Authorization, "Bearer $token")
    }
}
```

## API Service Pattern

```kotlin
// services/BackendApiService.kt
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.http.*

class BackendApiService(
    private val client: HttpClient,
    private val baseUrl: String = System.getenv("BACKEND_URL")
) {

    // User operations
    suspend fun getUser(telegramId: Long): User? {
        return runCatching {
            client.get("$baseUrl/users/telegram/$telegramId").body<User>()
        }.getOrNull()
    }

    suspend fun createUser(telegramId: Long, name: String): User {
        return client.post("$baseUrl/users") {
            contentType(ContentType.Application.Json)
            setBody(CreateUserRequest(telegramId, name))
        }.body()
    }

    suspend fun updateUserSettings(userId: Long, settings: UserSettings): User {
        return client.put("$baseUrl/users/$userId/settings") {
            contentType(ContentType.Application.Json)
            setBody(settings)
        }.body()
    }

    // Chat/Message operations
    suspend fun saveMessage(chatId: Long, message: SaveMessageRequest): SavedMessage {
        return client.post("$baseUrl/chats/$chatId/messages") {
            contentType(ContentType.Application.Json)
            setBody(message)
        }.body()
    }

    suspend fun getMessages(chatId: Long, page: Int = 1): PaginatedResponse<SavedMessage> {
        return client.get("$baseUrl/chats/$chatId/messages") {
            parameter("page", page)
            parameter("limit", 20)
        }.body()
    }

    // Health check
    suspend fun healthCheck(): Boolean {
        return runCatching {
            client.get("$baseUrl/health").status.isSuccess()
        }.getOrDefault(false)
    }
}
```

## DTOs (Data Transfer Objects)

```kotlin
// models/ApiModels.kt
import kotlinx.serialization
Files: 2
Size: 20.8 KB
Complexity: 26/100
Category: Backend & APIs

Related in Backend & APIs