Kotlin (OkHttp + Ktor)
OkHttp coroutines + kotlinx.serialization, and the Ktor client equivalent for multiplatform.
Kotlin developers reach for OkHttp on the JVM (Android, server-side) and Ktor for multiplatform projects (Android + iOS shared code). Both work fine with the Go REST API. This guide covers OkHttp + kotlinx.serialization first, then shows the Ktor client equivalent.
Dependencies
Add OkHttp, kotlinx-serialization, and coroutines to your build. The serialization plugin is what generates the@Serializable glue at compile time.
// build.gradle.kts
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
}
plugins {
kotlin("plugin.serialization") version "2.0.21"
}
Setup
OneOkHttpClient per process is enough; it has its own thread pool and connection pool. Read the token from env so it does not end up in source control.
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import kotlinx.serialization.json.Json
import kotlinx.coroutines.*
object GoRest {
const val API = "https://gorest.co.in/public/v2"
val token: String = System.getenv("GOREST_TOKEN")
?: error("Set GOREST_TOKEN")
val http = OkHttpClient.Builder()
.callTimeout(15, java.util.concurrent.TimeUnit.SECONDS)
.build()
val json = Json { ignoreUnknownKeys = true }
}
Define a DTO
A@Serializable data class is the cleanest way to bind the JSON. Use defaults for fields that are server-assigned (id) so you can use the same class for create-input and read-output.
@kotlinx.serialization.Serializable
data class User(
val id: Int = 0,
val name: String,
val email: String,
val gender: String,
val status: String,
)
GET with coroutines
Wrap blocking I/O inwithContext(Dispatchers.IO) so the call works inside a coroutine. The.use { } block guarantees the response is closed.
suspend fun listUsers(status: String = "active"): List<User> = withContext(Dispatchers.IO) {
val req = Request.Builder()
.url("${GoRest.API}/users?status=$status")
.header("Authorization", "Bearer ${GoRest.token}")
.header("Accept", "application/json")
.build()
GoRest.http.newCall(req).execute().use { res ->
if (!res.isSuccessful) error("List -> ${res.code}")
GoRest.json.decodeFromString<List<User>>(res.body!!.string())
}
}
Create, update, delete
The three writes usepost,patch,delete on the OkHttp builder. Always include theContent-Type header on requests with a body.
private val jsonMedia = "application/json".toMediaType()
suspend fun createUser(input: User): User = withContext(Dispatchers.IO) {
val payload = GoRest.json.encodeToString(input)
val req = Request.Builder()
.url("${GoRest.API}/users")
.header("Authorization", "Bearer ${GoRest.token}")
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.post(payload.toRequestBody(jsonMedia))
.build()
GoRest.http.newCall(req).execute().use { res ->
if (res.code != 201) error("Create -> ${res.code}: ${res.body?.string()}")
GoRest.json.decodeFromString<User>(res.body!!.string())
}
}
suspend fun updateUserStatus(id: Int, status: String) = withContext(Dispatchers.IO) {
val req = Request.Builder()
.url("${GoRest.API}/users/$id")
.header("Authorization", "Bearer ${GoRest.token}")
.header("Content-Type", "application/json")
.patch("""{"status":"$status"}""".toRequestBody(jsonMedia))
.build()
GoRest.http.newCall(req).execute().use { check(it.isSuccessful) }
}
suspend fun deleteUser(id: Int) = withContext(Dispatchers.IO) {
val req = Request.Builder()
.url("${GoRest.API}/users/$id")
.header("Authorization", "Bearer ${GoRest.token}")
.delete()
.build()
GoRest.http.newCall(req).execute().use { check(it.code == 204) }
}
Retry on rate limits
OkHttp interceptors run on every request. Drop one in for transparent retry on 429, so your callers do not have to know:
class RateLimitInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
repeat(3) { attempt ->
val res = chain.proceed(chain.request())
if (res.code != 429) return res
val wait = res.header("X-RateLimit-Reset")?.toIntOrNull() ?: 1
res.close()
Thread.sleep(maxOf(wait, 1) * 1000L)
}
return chain.proceed(chain.request())
}
}
// wire it on the client:
val http = OkHttpClient.Builder()
.addInterceptor(RateLimitInterceptor())
.build()
Same calls in Ktor
If you are writing multiplatform code (Android + iOS shared module, or KMP web), Ktor is the right client. The shape is similar, but the framework owns more of the plumbing: automatic content negotiation, authentication plugins, and engines for each platform.
// build.gradle.kts
// implementation("io.ktor:ktor-client-core:2.3.12")
// implementation("io.ktor:ktor-client-cio:2.3.12")
// implementation("io.ktor:ktor-client-content-negotiation:2.3.12")
// implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.12")
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.serialization.kotlinx.json.*
val client = HttpClient(CIO) {
defaultRequest {
header("Authorization", "Bearer ${System.getenv("GOREST_TOKEN")}")
header("Accept", "application/json")
}
install(ContentNegotiation) { json() }
}
suspend fun listUsers(): List<User> =
client.get("https://gorest.co.in/public/v2/users?status=active").body()
Tips
- Use code .use { } | on every OkHttp response. Skipping it leaks connections.
Json { ignoreUnknownKeys = true }is forgiving when the API adds new fields, so your code keeps compiling. The default rejects unknown keys.- In Android, configure OkHttp with code Cache | to keep recent responses on disk; it dramatically lowers your rate-limit usage on screens that read the same data often.
- The Ktor code defaultRequest | block sets headers on every call without polluting individual call sites.
Keep going
JavaScript (Fetch API)
Browser-native fetch with async/await, bearer-token auth, error handling, and pagination.
Node.js
Server-side requests with global fetch and axios: retries, env-loaded tokens, streaming JSON.
Python (requests)
Calls with the requests library, JSON bodies, query filtering, and dataclass parsing.