Skip to main content
The HTTP Client library provides a configured Ktor HttpClient for network operations with built-in JSON serialization, timeout handling, and authentication support.

Core Interface

HttpClientProvider

Simple interface for accessing the configured HTTP client.
interface HttpClientProvider {
    fun getClient(): HttpClient
}
Purpose:
  • Abstracts HTTP client creation and configuration
  • Enables easy mocking in tests
  • Provides a single source of truth for client configuration

Implementation

RealHttpClientProvider

Production implementation using Ktor CIO engine with pre-configured plugins.
object RealHttpClientProvider : HttpClientProvider {

    private val httpClient by lazy {
        createClient(CIO.create())
    }

    fun createClient(engine: HttpClientEngine): HttpClient {
        return HttpClient(engine) {
            install(ContentNegotiation) {
                json(
                    Json {
                        isLenient = true
                        ignoreUnknownKeys = true
                    }
                )
            }
            install(HttpTimeout) {
                requestTimeoutMillis = 3000L
            }
        }
    }

    override fun getClient(): HttpClient {
        return httpClient
    }
}

Configuration Details

Configures automatic JSON serialization/deserialization using kotlinx.serialization:
  • isLenient = true: Allows lenient parsing of JSON (quotes, etc.)
  • ignoreUnknownKeys = true: Ignores JSON fields not defined in the data class
This prevents crashes when the API returns additional fields not yet mapped in your models.
Sets timeout configurations for all requests:
  • requestTimeoutMillis = 3000L: 3-second timeout for entire request
This prevents hanging requests and provides quick failure feedback.
Uses Ktor’s CIO (Coroutine I/O) engine:
  • Pure Kotlin implementation
  • No additional platform dependencies
  • Efficient coroutine-based I/O

Authentication

AccessTokenProvider

Provides authentication headers for API requests.
object AccessTokenProvider {
    fun getAccessTokenHeader(): Pair<String, String> {
        return "Authorization" to "Bearer sghe50o5rkn9n9n1azs0ectmxnjjls7r9i1tksz3"
    }
}
In production, access tokens should be retrieved from secure storage, not hardcoded. This example shows the pattern for demonstration purposes.

Usage Examples

Making HTTP Requests in Repository

Example from RealProductRepository showing HTTP client usage:
internal class RealProductRepository(
    private val httpClient: HttpClient
) : ProductRepository {
    override suspend fun getProducts(): Answer<List<Product>, Unit> {
        return try {
            val response =
                httpClient.get("https://api.json-generator.com/templates/Vc6TVI8VwZNT/data") {
                    headers {
                        append(HttpHeaders.ContentType, ContentType.Application.Json.toString())
                        val accessTokenHeader = AccessTokenProvider.getAccessTokenHeader()
                        append(accessTokenHeader.first, accessTokenHeader.second)
                    }
                }
            if (response.status.isSuccess()) {
                handleSuccessfulProductsResponse(response)
            } else {
                Answer.Error(Unit)
            }
        } catch (t: Throwable) {
            Answer.Error(Unit)
        }
    }

    private suspend fun handleSuccessfulProductsResponse(httpResponse: HttpResponse): Answer<List<Product>, Unit> {
        val responseBody = httpResponse.body<List<JsonProductResponseDTO>>()
        return Answer.Success(mapProducts(responseBody))
    }

    private fun mapProducts(jsonProducts: List<JsonProductResponseDTO>): List<Product> {
        return jsonProducts.map { jsonProduct ->
            Product(
                jsonProduct.id.toString(),
                jsonProduct.name,
                Money(jsonProduct.price, jsonProduct.currency),
                jsonProduct.imageUrl
            )
        }
    }
}

Step-by-Step HTTP Request

1

Inject HttpClient

Receive the HttpClient through dependency injection:
class RealProductRepository(
    private val httpClient: HttpClient
) : ProductRepository
2

Make the request

Use Ktor’s extension functions for HTTP methods:
val response = httpClient.get("https://api.example.com/endpoint") {
    headers {
        append(HttpHeaders.ContentType, ContentType.Application.Json.toString())
        val accessTokenHeader = AccessTokenProvider.getAccessTokenHeader()
        append(accessTokenHeader.first, accessTokenHeader.second)
    }
}
3

Check response status

Verify the response was successful:
if (response.status.isSuccess()) {
    // Handle success
} else {
    // Handle error
}
4

Deserialize response

Automatic deserialization using kotlinx.serialization:
val responseBody = httpResponse.body<List<JsonProductResponseDTO>>()
5

Map to domain model

Convert DTOs to domain models:
val products = mapProducts(responseBody)
return Answer.Success(products)

Error Handling Pattern

override suspend fun getData(): Answer<Data, Unit> {
    return try {
        val response = httpClient.get(url) {
            // Configuration
        }
        if (response.status.isSuccess()) {
            val body = response.body<DataDTO>()
            Answer.Success(mapToDomain(body))
        } else {
            Answer.Error(Unit)
        }
    } catch (t: Throwable) {
        Answer.Error(Unit)
    }
}
This pattern uses the Answer type from the foundations library to represent success or failure results.

Testing

Creating Test Clients

The createClient function accepts a custom engine for testing:
val testEngine = MockEngine { request ->
    respond(
        content = """[{"id": 1, "name": "Test Product"}]""",
        status = HttpStatusCode.OK,
        headers = headersOf(HttpHeaders.ContentType, "application/json")
    )
}

val testClient = RealHttpClientProvider.createClient(testEngine)
Use Ktor’s MockEngine in tests to avoid actual network calls and control response scenarios.

Common HTTP Operations

GET Requests

httpClient.get("url") {
    headers { /* ... */ }
}
Retrieve data from the server.

POST Requests

httpClient.post("url") {
    contentType(ContentType.Application.Json)
    setBody(data)
}
Send data to the server.

PUT Requests

httpClient.put("url") {
    contentType(ContentType.Application.Json)
    setBody(updatedData)
}
Update existing resources.

DELETE Requests

httpClient.delete("url") {
    headers { /* ... */ }
}
Remove resources from the server.

Best Practices

Dependency Injection: Always inject HttpClient rather than accessing RealHttpClientProvider directly for better testability.
Timeout Configuration: The 3-second timeout is suitable for most API calls. Adjust based on your specific needs.
Error Handling: Always wrap HTTP calls in try-catch blocks to handle network failures gracefully.
The HttpClient is configured as a singleton (lazy initialization). Avoid creating multiple instances unnecessarily.

Configuration Customization

For custom configurations, extend or wrap the provider:
object CustomHttpClientProvider : HttpClientProvider {
    private val httpClient by lazy {
        HttpClient(CIO.create()) {
            install(ContentNegotiation) { /* custom config */ }
            install(HttpTimeout) { 
                requestTimeoutMillis = 5000L // Different timeout
            }
            install(Logging) { 
                level = LogLevel.ALL
            }
        }
    }

    override fun getClient(): HttpClient = httpClient
}
Consider adding the Logging plugin during development to debug HTTP requests and responses.

Build docs developers (and LLMs) love