Go REST

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

Related guides

Keep going

Back to all guides Try it in the console