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 endpointhttpClient— 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 classesgrouping— how client classes are organizedgenerateTestHarness— whether to generate test harness functionsdisplayName— 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()vsclient.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}→UsersClientGET /users→UsersClientPOST /vinyls→VinylsClientGET /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/apiinbaseUrl.
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.