name: kmp-api-services description: "KMP API service patterns using Ktor for HTTP networking, DTOs, and serialization. Use when: (1) Implementing API services with Ktor Client, (2) Designing type-safe DTOs with kotlinx.serialization, (3) Mapping remote data to domain models, (4) Configuring HTTP clients with retries and timeouts, (5) Testing API services with MockEngine. Keywords: Ktor, DTOs, API services, serialization, HTTP client"
KMP API Services
API service patterns for Kotlin Multiplatform using Ktor to keep remote APIs structured, testable, and decoupled from domain models.
When to Use This Skill
- Implementing or modifying API services (Ktor Client).
- Defining Request/Response DTOs with Kotlinx Serialization.
- Mapping remote data to domain models via
asDomain(). - Configuring Ktor Client (engines, JSON, logging, timeouts).
- Triggers: "API service", "Ktor", "remote", "HTTP", "DTO", "request/response", "serialization".
Related Skills
- @kmp-data-layer: Handles the
Either<RepoError, T>boundary and repository implementation. - @kmp-architecture: Guidelines for module structure and vertical slicing.
Mode Detection
| User Request | Reference File | Load When |
|---|---|---|
| "Test API service" / "MockEngine" | testing.md | MANDATORY - Read before testing |
| "Configure Ktor client" / "Setup HTTP client" | ktor-configuration.md | MANDATORY - Read before configuration |
MANDATORY - READ ENTIRE FILE: Before testing API services, you MUST read testing.md (~48 lines) for MockEngine patterns, DTO round-trip tests, and error simulation.
MANDATORY - READ ENTIRE FILE: Before configuring Ktor client, you MUST read ktor-configuration.md (~41 lines) for centralized client factory, platform engines, and best practices.
Do NOT load testing.md for configuration-only tasks.
Do NOT load ktor-configuration.md for testing-only tasks.
Critical Patterns
1. API Service Boundary
API services return raw data or DTOs. They NEVER return Result or Either. Error handling is deferred to the Repository layer.
interface JobApiService {
suspend fun getJobs(request: GetJobsRequest): GetJobsResponse
}
2. DTO Naming & Serialization
- Use
RequestandResponsesuffixes. - Always use
@Serializableand@SerialName.
@Serializable
data class JobResponse(
@SerialName("id") val id: String,
@SerialName("title") val title: String
) {
fun asDomain(): Job = Job(id = id, title = title)
}
3. Repository Integration
Repositories wrap API calls in Either.catch and map results.
override suspend fun getJobs(): Either<RepoError, List<Job>> =
Either.catch {
api.getJobs(GetJobsRequest()).jobs.map { it.asDomain() }
}.mapLeft { it.toRepoError() }
Critical Guardrails
- NEVER return
EitherorResultfrom an API service → return raw DTOs and defer error handling to the Repository (reason: maintains layer separation). - NEVER leak serialization annotations (
@Serializable) into domain models → use separate DTO classes in:data(reason: decouples domain logic from API changes). - NEVER use domain models directly as API response/request types → always define explicit DTOs (reason: prevents API changes from breaking domain logic).
- NEVER hardcode Dispatchers inside services → use
suspendfunctions and let the caller manage the context (reason: improves testability and follows structured concurrency). - NEVER skip
@SerialName→ always use explicit JSON key mapping (reason: protects against property renaming/obfuscation and maintains API contract). - NEVER share DTOs between features → each feature must own its DTOs in its
:datamodule (reason: maintains vertical slice independence and prevents tight coupling). - NEVER catch exceptions in API services → let exceptions propagate to the repository layer (reason: repositories use
Either.catchto establish a consistent error boundary). - NEVER use public for implementation classes → use
internal classwith a public factory function (reason: supports Gradle compilation avoidance).
Decision Framework
Before implementing API services, ask yourself:
What data structure does the API return?
- JSON response → Define
@SerializableDTO with@SerialNameannotations - Nested objects → Create separate DTO classes for each level
- Lists/arrays → Use
List<DTO>in response, map toImmutableListin domain
- JSON response → Define
How should errors be handled?
- API service → Let exceptions propagate (NO try/catch)
- Repository layer → Wrap with
Either.catch { api.call() }.mapLeft { it.toRepoError() } - Network errors → Caught by repository as
RepoError.Network - HTTP errors → Caught by repository as
RepoError.Http(code, message)
What testing strategy is needed?
- Use Ktor MockEngine for API service tests
- Test DTO serialization/deserialization with property tests
- Test domain mapping with
dto.asDomain()property tests (100% coverage)
Essential Workflows
Workflow 1: Creating a New API Service with Ktor Client
To add a new API service following the vertical slice architecture:
Define the service interface in the feature's
:datamodule:// features/jobs/data/src/commonMain/.../JobApiService.kt interface JobApiService { suspend fun getJobs(): JobResponse }Implement the interface using an
internal class:internal class JobApiServiceImpl( private val httpClient: HttpClient ) : JobApiService { override suspend fun getJobs(): JobResponse = httpClient.get("https://api.example.com/jobs").body() }Create a public factory function to expose the service:
fun JobApiService(httpClient: HttpClient): JobApiService = JobApiServiceImpl(httpClient)Register in Koin module:
val jobModule = module { factory { JobApiService(httpClient = get()) } }
Cross-references: @kmp-data-layer (repository integration), @kmp-di (Koin wiring)
Workflow 2: Defining DTOs with Kotlinx Serialization
To define type-safe request/response models:
Create data class for the API response in
remote/dto/:@Serializable data class JobResponse( @SerialName("jobs") val jobs: List<JobDto> )Use @SerialName for all properties to protect against obfuscation:
@Serializable data class JobDto( @SerialName("id") val id: String, @SerialName("job_title") val title: String, @SerialName("company_name") val company: String )Add asDomain() mapper to convert to domain models:
fun JobDto.asDomain(): Job = Job( id = id, title = title, company = company )
Cross-references: @kmp-domain (domain models), @kmp-data-layer (mappers)
Workflow 3: Configuring Ktor Client with ContentNegotiation
To set up the Ktor client for JSON serialization:
Centralize configuration in
:core:httpclient:fun createHttpClient(engine: HttpClientEngine) = HttpClient(engine) { install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true isLenient = true }) } install(HttpTimeout) { requestTimeoutMillis = 15000 } }Inject HttpClient into API services via DI.
Cross-references: @kmp-gradle (build configuration), @kmp-di (singleton registration)
Quick Reference
| Pattern | Purpose | Example |
|---|---|---|
| GET Request | Fetch data | httpClient.get("url").body<T>() |
| POST Request | Send data | httpClient.post("url") { setBody(dto) }.body<T>() |
| Query Parameters | Filter data | parameter("key", value) |
| @SerialName | Map JSON key | @SerialName("json_key") val property: String |
| asDomain() | DTO→Domain | fun Dto.asDomain() = DomainModel(...) |
| MockEngine | Test API | HttpClient(MockEngine { respond(...) }) |
Cross-References
Skills (by Category)
Data Layer
| Skill | Purpose | Link |
|---|---|---|
| @kmp-data-layer | Repository patterns with Either | SKILL.md |
| @kmp-domain | Domain models and business logic | SKILL.md |
Architecture
| Skill | Purpose | Link |
|---|---|---|
| @kmp-architecture | Module structure and vertical slicing | SKILL.md |
| @kmp-critical-patterns | Quick reference for 6 core patterns | SKILL.md |
DI & Infrastructure
| Skill | Purpose | Link |
|---|---|---|
| @kmp-di | Koin dependency injection configuration | SKILL.md |
| @kmp-gradle | Convention plugins and build setup | SKILL.md |
| @kmp-developer | General development and refactoring | SKILL.md |
| @ktor-backend | Server-side Ktor API implementation | SKILL.md |
Testing
| Skill | Purpose | Link |
|---|---|---|
| @kmp-testing-patterns | MockEngine and property-based tests | SKILL.md |
Documents
| Document | Purpose | Link |
|---|---|---|
| @kmp-architecture | Architecture master reference | Architecture patterns |
| @kmp-critical-patterns | 6 core patterns guide | Quick reference |
| ktor-configuration.md | Detailed Ktor setup guide | ktor-configuration.md |