| name | kotlin-ktor-patterns |
| description | Ktor server patterns for Kotlin. Use when building Ktor HTTP servers, configuring plugins (Auth, CORS, StatusPages), adding Koin DI, WebSockets, or writing testApplication tests. |
| origin | MCC |
Ktor Server Patterns
Comprehensive Ktor patterns for building robust, maintainable HTTP servers with Kotlin coroutines.
When to Activate
- Building Ktor HTTP servers
- Configuring Ktor plugins (Auth, CORS, ContentNegotiation, StatusPages)
- Implementing REST APIs with Ktor
- Setting up dependency injection with Koin
- Writing Ktor integration tests with testApplication
- Working with WebSockets in Ktor
Application Structure
Standard Ktor Project Layout
src/main/kotlin/
+-- com/example/
+-- Application.kt # Entry point, module configuration
+-- plugins/
| +-- Routing.kt # Route definitions
| +-- Serialization.kt # Content negotiation setup
| +-- Authentication.kt # Auth configuration
| +-- StatusPages.kt # Error handling
| +-- CORS.kt # CORS configuration
+-- routes/
| +-- UserRoutes.kt # /users endpoints
| +-- AuthRoutes.kt # /auth endpoints
+-- models/
| +-- User.kt # Domain models
| +-- ApiResponse.kt # Response envelopes
+-- services/
| +-- UserService.kt # Business logic
+-- repositories/
| +-- UserRepository.kt # Data access interface
+-- di/
+-- AppModule.kt # Koin modules
Application Entry Point
fun main() {
embeddedServer(Netty, port = 8080, module = Application::module).start(wait = true)
}
fun Application.module() {
configureSerialization()
configureAuthentication()
configureStatusPages()
configureCORS()
configureDI()
configureRouting()
}
Routing DSL
Basic Routes
fun Application.configureRouting() {
routing {
userRoutes()
authRoutes()
healthRoutes()
}
}
fun Route.userRoutes() {
val userService by inject<UserService>()
route("/users") {
get {
val users = userService.getAll()
call.respond(users)
}
get("/{id}") {
val id = call.parameters["id"]
?: return@get call.respond(HttpStatusCode.BadRequest, "Missing id")
val user = userService.getById(id)
?: return@get call.respond(HttpStatusCode.NotFound)
call.respond(user)
}
post {
val request = call.receive<CreateUserRequest>()
val user = userService.create(request)
call.respond(HttpStatusCode.Created, user)
}
put("/{id}") {
val id = call.parameters["id"]
?: return@put call.respond(HttpStatusCode.BadRequest, "Missing id")
val request = call.receive<UpdateUserRequest>()
val user = userService.update(id, request)
?: return@put call.respond(HttpStatusCode.NotFound)
call.respond(user)
}
delete("/{id}") {
val id = call.parameters["id"]
?: return@delete call.respond(HttpStatusCode.BadRequest, )
deleted = userService.delete(id)
(deleted) call.respond(HttpStatusCode.NoContent)
call.respond(HttpStatusCode.NotFound)
}
}
}
Route Organization with Authenticated Routes
fun Route.userRoutes() {
route("/users") {
get { }
get("/{id}") { }
authenticate("jwt") {
post { }
put("/{id}") { }
delete("/{id}") { }
}
}
}
Content Negotiation & Serialization
fun Application.configureSerialization() {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = false
ignoreUnknownKeys = true
encodeDefaults = true
explicitNulls = false
})
}
}
Serializable Models
@Serializable
data class UserResponse(
val id: String,
val name: String,
val email: String,
val role: Role,
@Serializable(with = InstantSerializer::class)
val createdAt: Instant,
)
@Serializable
data class CreateUserRequest(val name: String, val email: String, val role: Role = Role.USER)
@Serializable
data class ApiResponse<T>(
val success: Boolean,
val data: T? = null,
val error: String? = null,
) {
companion object {
fun <T> ok(data: T): ApiResponse<T> = ApiResponse(success = true, data = data)
fun <T> error(message: String): ApiResponse<T> = ApiResponse(success = false, error = message)
}
}
@Serializable
data class PaginatedResponse<>( : List<T>, total: , page: , limit: )
Custom Serializers
object InstantSerializer : KSerializer<Instant> {
override val descriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): Instant = Instant.parse(decoder.decodeString())
}
Quick Reference: Ktor Patterns
| Pattern | Description |
|---|
route("/path") { get { } } | Route grouping with DSL |
call.receive<T>() | Deserialize request body |
call.respond(status, body) | Send response with status |
call.parameters["id"] | Read path parameters |
call.request.queryParameters["q"] | Read query parameters |
install(Plugin) { } | Install and configure plugin |
authenticate("name") { } | Protect routes with auth |
by inject<T>() | Koin dependency injection |
testApplication { } | Integration testing |
Remember: Ktor is designed around Kotlin coroutines and DSLs. Keep routes thin, push logic to services, and use Koin for dependency injection. Test with testApplication for full integration coverage.
Reference Files
- auth-and-plugins.md — JWT authentication, auth routes, StatusPages error handling, CORS configuration, Koin DI setup, and request validation
- websockets-and-testing.md — WebSocket implementation, testApplication route testing, authenticated route testing, and application.yaml configuration