Ktor Patterns
Application Setup
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
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
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
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
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
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
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