Skip to main content

Generate code

After building the project, the following files are generated in <api-package>.generated package:

Client class

<ApiName>Client.kt — Client class with all API methods

Test harness

<ApiName>TestHarness.kt — Test utility functions

Simple test harness

fun myApiTest(
baseUrl: String = MyApiSpec.baseUrl,
client: HttpClient = MyApiSpec.httpClient,
block: suspend MyClient.() -> Unit
)

Usage:

@Test
fun `get users`() = myApiTest {
val response = getUsers()
response.requireSuccess()
}

Test harness with setup/teardown

fun <T> myApiTest(
baseUrl: String = MyApiSpec.baseUrl,
client: HttpClient = MyApiSpec.httpClient,
setUp: suspend MyClient.() -> T,
tearDown: suspend MyClient.(T) -> Unit = {},
block: suspend MyClient.(T) -> Unit
)

Usage:

@Test
fun `update user`() = myApiTest(
setUp = {
// Create test user, return its ID
createUser { name = "Test" }.body.id
},
tearDown = { userId ->
// Clean up
deleteUser(userId)
}
) { userId ->
// Test with the created user
val response = updateUser(userId) { name = "Updated" }
assertEquals("Updated", response.body.name)
}

Customizing code generation

Runtime vs. codegen configuration

ApiSpec owns runtime configuration — properties that the generated code uses at execution time:

  • baseUrl — the base URL for the API endpoint
  • httpClient — the HTTP client instance

The @GenerateApi annotation owns codegen configuration — directives that control how the KSP processor generates code:

  • scanPackages — packages to scan for request classes
  • grouping — how client classes are organized
  • generateTestHarness — whether to generate test harness functions
  • displayName — human-readable API name used in generated KDoc and class names

When @GenerateApi is used without explicit arguments, all codegen properties use their defaults.

Scan custom packages

By default, the processor scans <api-package>.models for request classes. You can specify custom packages using the @GenerateApi annotation:

import dev.kolibrium.api.ksp.annotations.GenerateApi

@GenerateApi(scanPackages = ["com.example.api.requests", "com.example.api.queries"])
object MyApiSpec : ApiSpec(baseUrl = "https://api.example.com")

An empty scanPackages array (the default) means "use the convention default": the <api-package>.models subpackage.

Client grouping

The processor supports two client organization modes, configured via the @GenerateApi annotation:

SingleClient (default)

All endpoints are generated in a single client class:

@GenerateApi
object MyApiSpec : ApiSpec(baseUrl = "https://api.example.com")

Generates:

class MyClient(client: HttpClient, baseUrl: String) {
suspend fun getUser(id: Int): ApiResponse<User>
suspend fun listUsers(): ApiResponse<UserList>
suspend fun createVinyl(block: CreateVinylRequest.() -> Unit): ApiResponse<Vinyl>
// ... all methods in one class
}

ByPrefix

This feature automatically organizes API client methods into separate client classes based on the first path segment of their endpoints. This helps with:

  • Code Organization: Large APIs with many endpoints get split into logical, resource-based client classes
  • Discoverability: Developers can navigate client.users.getUser() vs client.vinyls.createVinyl() more intuitively
  • Separation of Concerns: Each resource domain gets its own client class
import dev.kolibrium.api.ksp.annotations.ClientGrouping
import dev.kolibrium.api.ksp.annotations.GenerateApi

@GenerateApi(grouping = ClientGrouping.ByPrefix)
object MyApiSpec : ApiSpec(baseUrl = "https://api.example.com")

Given these requests:

  • GET /users/{id}UsersClient
  • GET /usersUsersClient
  • POST /vinylsVinylsClient
  • GET /vinyls/{id}VinylsClient

It groups related endpoints by their first path segment and generates:

// Group clients
class UsersClient(client: HttpClient, baseUrl: String) {
suspend fun getUser(id: Int): ApiResponse<User>
suspend fun listUsers(): ApiResponse<UserList>
}

class VinylsClient(client: HttpClient, baseUrl: String) {
suspend fun getVinyl(id: Int): ApiResponse<Vinyl>
suspend fun createVinyl(block: CreateVinylRequest.() -> Unit): ApiResponse<Vinyl>
}

// Root aggregator client that contains all group clients as properties
class MyClient(client: HttpClient, baseUrl: String) {
val users = UsersClient(client, baseUrl)
val vinyls = VinylsClient(client, baseUrl)
}

Usage:

val client = MyClient(MyApiSpec.httpClient, "https://api.example.com")
client.users.getUser(1)
client.vinyls.createVinyl { artist = "Pink Floyd" }

Note: If all paths start with /api/..., everything groups under a single ApiClient, defeating the purpose of grouping. The solution is to put /api in baseUrl.

Display name

The displayName parameter controls the human-readable API name used in generated KDoc comments and class names. When not specified, the name is derived automatically from the class name by stripping common suffixes (ApiSpec, Spec).

For example, VinylStoreApiSpec produces displayName = "VinylStore", which results in:

  • Client class: VinylStoreClient
  • KDoc: "HTTP client for the VinylStore API."

To override the derived name:

@GenerateApi(displayName = "Record Shop")
object MyApiSpec : ApiSpec(baseUrl = "https://api.example.com")

This produces RecordShopClient and KDoc referencing "Record Shop API."

KDoc generation

The processor generates KDoc comments on client methods, test harness functions, and sealed result types. Each generated request function gets a one-liner description plus @param tags indicating whether each parameter is a path, query, or header parameter.

Example of generated KDoc:

/**
* Performs a GET request to /users/{userId}/items.
*
* @param userId path parameter — substituted into `/users/{userId}/items`
* @param status query parameter
* @param correlationId header: `X-Correlation-ID`
*/
public suspend fun getUserItems(
userId: Int,
status: String? = null,
correlationId: String? = null,
): ApiResponse<ItemDto>

Custom HTTP client

By default, the generated test harness uses the httpClient from your ApiSpec, which defaults to defaultHttpClient — a pre-configured client with JSON content negotiation, logging, and timeouts. You can customize the HTTP client by overriding httpClient:

import dev.kolibrium.api.core.ApiSpec
import dev.kolibrium.api.ksp.annotations.GenerateApi
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json

@GenerateApi
object MyApiSpec : ApiSpec(baseUrl = "https://api.example.com") {
override val httpClient = HttpClient(CIO) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
prettyPrint = true
})
}
}
}

Global headers

For headers that should be included in every request (e.g., client version, default content type), use Ktor's DefaultRequest plugin instead of adding @Header to each request class:

import dev.kolibrium.api.core.ApiSpec
import dev.kolibrium.api.ksp.annotations.GenerateApi
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.request.header
import io.ktor.serialization.kotlinx.json.json

@GenerateApi
object MyApiSpec : ApiSpec(baseUrl = "https://api.example.com") {
override val httpClient = HttpClient(CIO) {
install(ContentNegotiation) { json() }
defaultRequest {
header("X-Client-Version", "1.0")
header("Accept", "application/json")
}
}
}

Every request through this client automatically includes those headers.