Google Sheets as a REST API in Kotlin
Fetch and query Google Sheet data in Kotlin using SheetsAPI with ktor-client, typed data classes, coroutines, and Spring Boot examples.
3 min read
Kotlin's data classes, coroutines, and null safety make it an excellent language for consuming REST APIs cleanly. SheetsAPI returns standard JSON, so you can deserialize directly into Kotlin data classes with full type safety.
Prerequisites
- Kotlin 1.9+ with JVM or Android target
- A Google Sheet with a header row
- A SheetsAPI account and a
YOUR_USER_KEYkey from the dashboard
1. Data classes
// models/SheetResponse.kt
data class SheetMeta(val total: Int, val limit: Int, val offset: Int)
data class SheetResponse<T>(
val data: List<T>,
val meta: SheetMeta
)
data class Product(
val name: String,
val price: String,
val category: String,
val stock: String
)2. Basic fetch with ktor-client
Add to build.gradle.kts:
dependencies {
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.request.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
val client = HttpClient(CIO) {
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
}
}
suspend fun fetchProducts(userKey: String, apiKey: String): SheetResponse<Product> {
return client.get("https://sheetsapi.gkit.mreshank.com/api/spreadsheets/$userKey/Products") {
parameter("limit", 20)
parameter("sort", "name")
header("Authorization", "Bearer $apiKey")
}.body()
}Mark your data classes with @Serializable when using kotlinx.serialization:
import kotlinx.serialization.Serializable
@Serializable data class SheetMeta(val total: Int, val limit: Int, val offset: Int)
@Serializable data class SheetResponse<T>(val data: List<T>, val meta: SheetMeta)
@Serializable data class Product(val name: String, val price: String, val category: String)3. Reusable client class
class SheetsApiClient(
private val userKey: String,
private val apiKey: String? = null
) {
private val http = HttpClient(CIO) {
install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
}
private val base = "https://sheetsapi.gkit.mreshank.com/api/spreadsheets"
suspend inline fun <reified T : Any> query(
sheet: String,
limit: Int = 20,
offset: Int = 0,
search: String? = null,
sort: String? = null,
fields: String? = null
): SheetResponse<T> = http.get("$base/$userKey/$sheet") {
parameter("limit", limit)
parameter("offset", offset)
search?.let { parameter("search", it) }
sort?.let { parameter("sort", it) }
fields?.let { parameter("fields", it) }
apiKey?.let { header("Authorization", "Bearer $it") }
}.body()
suspend inline fun <reified T : Any> fetchAll(sheet: String, pageSize: Int = 100): List<T> {
val all = mutableListOf<T>()
var offset = 0
while (true) {
val page = query<T>(sheet, limit = pageSize, offset = offset)
all += page.data
offset += pageSize
if (offset >= page.meta.total) break
}
return all
}
fun close() = http.close()
}Usage:
val client = SheetsApiClient("YOUR_USER_KEY", apiKey = "sk_your_api_key_here")
// Search and sort
val electronics = client.query<Product>(
"Products",
limit = 50,
search = "category:electronics",
sort = "-price"
)
// Fetch everything
val allProducts = client.fetchAll<Product>("Products")
println("${allProducts.size} total products")
client.close()4. Using java.net.http (no dependency)
For lightweight scripts with no ktor dependency:
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import kotlinx.serialization.json.Json
import kotlinx.serialization.decodeFromString
val httpClient = HttpClient.newHttpClient()
val json = Json { ignoreUnknownKeys = true }
fun fetchProducts(userKey: String, apiKey: String): SheetResponse<Product> {
val uri = URI("https://sheetsapi.gkit.mreshank.com/api/spreadsheets/$userKey/Products?limit=20&sort=name")
val request = HttpRequest.newBuilder(uri)
.header("Authorization", "Bearer $apiKey")
.GET()
.build()
val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString())
check(response.statusCode() == 200) { "API error ${response.statusCode()}" }
return json.decodeFromString(response.body())
}5. Coroutine scope with error handling
import kotlinx.coroutines.*
fun main() = runBlocking {
val client = SheetsApiClient("YOUR_USER_KEY", "sk_your_api_key_here")
val result = runCatching {
client.query<Product>("Products", limit = 10, sort = "name")
}
result.fold(
onSuccess = { resp ->
println("Total: ${resp.meta.total}")
resp.data.forEach { println(" ${it.name} - ${it.price}") }
},
onFailure = { e ->
System.err.println("SheetsAPI error: ${e.message}")
}
)
client.close()
}6. Spring Boot integration
If you are using Spring Boot with Kotlin:
// SheetsApiService.kt
@Service
class SheetsApiService(
@Value("\${sheetsapi.user-key}") private val userKey: String,
@Value("\${sheetsapi.api-key:}") private val apiKey: String,
private val restClient: RestClient
) {
fun <T> query(sheet: String, responseType: Class<T>, search: String? = null): T {
return restClient.get()
.uri("https://sheetsapi.gkit.mreshank.com/api/spreadsheets/$userKey/$sheet") {
it.queryParamIfPresent("search", Optional.ofNullable(search))
}
.retrieve()
.body(responseType)!!
}
}# application.yml
sheetsapi:
user-key: YOUR_USER_KEY
api-key: sk_your_api_key_hereQuery parameter reference
| Parameter | Example | Description |
|---|---|---|
limit | 50 | Rows per page (default: 20, max: 500) |
offset | 0 | Row offset for pagination |
search | category:books | Filter by field:value |
sort | price or -price | Ascending / descending |
fields | name,price | Return only named columns |
Summary
| Concern | Approach |
|---|---|
| HTTP | ktor-client or java.net.http |
| Serialization | kotlinx.serialization or System.Text.Json |
| Types | Data classes + @Serializable |
| Async | Coroutines (suspend functions) |
| Error handling | runCatching / fold |
| Spring Boot | RestClient + @Value config |
SheetsAPI speaks plain JSON - Kotlin's data classes and coroutines make consuming it both type-safe and idiomatic.