# Kotlin Ktor Patterns

> When to activate: Ktor, Ktor routing, Ktor plugins, Ktor authentication, Ktor serialization, Ktor client, WebSockets, Koin

- Skill: `mattakushi432/kotlin-ktor-patterns` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattakushi432/kotlin-ktor-patterns`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattakushi432/kotlin-ktor-patterns/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Mattakushi432 (https://skillmd.com/u/mattakushi432)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mattakushi432/kotlin-ktor-patterns

---

# Ktor Patterns

## Application Setup

```kotlin
fun main() {
    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
        configurePlugins()
        configureRouting()
    }.start(wait = true)
}

fun Application.configurePlugins() {
    install(ContentNegotiation) { json() }
    install(RequestValidation) { validate<CreateUserRequest> { validateUser(it) } }
    install(StatusPages) { configureStatusPages() }
    install(CallLogging) { level = Level.INFO }
    install(Authentication) { configureJwt() }
    install(CORS) {
        allowHost("example.com", schemes = listOf("https"))
        allowHeader(HttpHeaders.ContentType)
        allowMethod(HttpMethod.Options)
    }
    install(RateLimit) {
        register(RateLimitName("api")) {
            rateLimiter(limit = 100, refillPeriod = 1.minutes)
        }
    }
}
```

## Routing

```kotlin
fun Application.configureRouting() {
    routing {
        route("/api/v1") {
            userRoutes()
            authenticate("jwt") {
                orderRoutes()
                adminRoutes()
            }
        }
    }
}

fun Route.userRoutes() {
    val service: UserService by inject()

    route("/users") {
        get {
            val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 0
            call.respond(service.findAll(page))
        }

        get("/{id}") {
            val id = call.parameters["id"]?.toLongOrNull()
                ?: return@get call.respond(HttpStatusCode.BadRequest, "Invalid id")
            val user = service.findById(id)
                ?: return@get call.respond(HttpStatusCode.NotFound)
            call.respond(user)
        }

        post {
            val request = call.receive<CreateUserRequest>()
            val user = service.create(request)
            call.respond(HttpStatusCode.Created, user)
        }
    }
}
```

## JWT Authentication

```kotlin
fun AuthenticationConfig.configureJwt() {
    jwt("jwt") {
        realm = "my-service"
        verifier(
            JWT.require(Algorithm.HMAC256(System.getenv("JWT_SECRET")))
                .withIssuer("my-service")
                .withAudience("api")
                .build()
        )
        validate { credential ->
            if (credential.payload.getClaim("userId").asLong() != null) JWTPrincipal(credential.payload)
            else null
        }
        challenge { _, _ ->
            call.respond(HttpStatusCode.Unauthorized, mapOf("error" to "Token invalid or expired"))
        }
    }
}

// Extract principal in route
val principal = call.principal<JWTPrincipal>()!!
val userId = principal.payload.getClaim("userId").asLong()
```

## Status Pages

```kotlin
fun StatusPagesConfig.configureStatusPages() {
    exception<ResourceNotFoundException> { call, cause ->
        call.respond(HttpStatusCode.NotFound, ErrorResponse(cause.message ?: "Not found"))
    }
    exception<ValidationException> { call, cause ->
        call.respond(HttpStatusCode.BadRequest, ErrorResponse(cause.message ?: "Validation failed"))
    }
    exception<Throwable> { call, cause ->
        call.application.log.error("Unhandled exception", cause)
        call.respond(HttpStatusCode.InternalServerError, ErrorResponse("Internal server error"))
    }
    status(HttpStatusCode.NotFound) { call, status ->
        call.respond(status, ErrorResponse("Route not found"))
    }
}
```

## Ktor Client

```kotlin
val client = HttpClient(CIO) {
    install(ContentNegotiation) { json() }
    install(HttpTimeout) {
        requestTimeoutMillis = 10_000
        connectTimeoutMillis = 5_000
    }
    install(HttpRequestRetry) {
        retryOnServerErrors(maxRetries = 3)
        exponentialDelay()
    }
    defaultRequest {
        url("https://api.example.com")
        header("Authorization", "Bearer ${apiKey}")
    }
}

// Usage
val response: UserResponse = client.get("/users/$id").body()
val created: UserResponse = client.post("/users") {
    contentType(ContentType.Application.Json)
    setBody(CreateUserRequest("Alice", "alice@example.com"))
}.body()
```

## WebSockets

```kotlin
fun Application.configureWebSockets() {
    install(WebSockets) { pingPeriod = 15.seconds }

    routing {
        webSocket("/ws/notifications") {
            val userId = call.principal<JWTPrincipal>()!!.payload.getClaim("userId").asLong()
            notificationRegistry.register(userId, this)
            try {
                for (frame in incoming) {
                    when (frame) {
                        is Frame.Text -> handleMessage(userId, frame.readText())
                        is Frame.Close -> break
                        else -> Unit
                    }
                }
            } finally {
                notificationRegistry.unregister(userId)
            }
        }
    }
}
```

## Dependency Injection with Koin

```kotlin
val appModule = module {
    single<UserRepository> { UserRepositoryImpl(get()) }
    single { UserService(get(), get()) }
    single { EmailService(getProperty("smtp.host")) }
    factory { UserController(get()) }
}

fun Application.configureDI() {
    install(Koin) {
        slf4jLogger()
        modules(appModule)
        properties(mapOf("smtp.host" to environment.config.property("app.smtp.host").getString()))
    }
}
```

## Key Rules
- Install plugins in `configurePlugins()`, routes in `configureRouting()` — separation keeps code navigable
- Use `call.receive<T>()` inside a try-catch or with `RequestValidation` plugin — raw deserialization throws
- `inject()` (Koin) vs `@Inject` — Ktor is not a DI framework; use Koin, Kodein, or manual wiring
- Ktor client is stateful — create once (singleton), share across requests, close on shutdown
- Use `rateLimit { }` blocks around public endpoints to prevent abuse

