Source profileQuality 83/100

affaan-m/ECC/skills/kotlin-ktor-patterns/SKILL.md

kotlin-ktor-patterns

Ktor server patterns including routing DSL, plugins, authentication, Koin DI, kotlinx.serialization, WebSockets, and testApplication testing.

Source repository stars
234,327
Declared platforms
0
Static risk flags
1
Last source update
2026-07-27
Source checked
2026-07-28

Decision brief

What it does—and where it fits

Comprehensive Ktor patterns for building robust, maintainable HTTP servers with Kotlin coroutines.

Best for

    Not for

    • Tasks that require unconfirmed production actions or broad system permissions.
    • Environments where the pinned source and install steps cannot be inspected.

    Compatibility matrix

    Platform support, with evidence labels

    PlatformStatusEvidenceWhat to check
    CodexNot declaredNo explicit evidencePortability before use
    Claude CodeNot declaredNo explicit evidencePortability before use
    CursorNot declaredNo explicit evidencePortability before use
    Gemini CLINot declaredNo explicit evidencePortability before use
    Open the compatibility checker

    Installation

    Inspect first. Install second.

    The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.

    Source-detected install commandSource
    npx skills add https://github.com/affaan-m/ECC --skill "skills/kotlin-ktor-patterns"
    Safe inspection promptEditorial

    Inspect the Agent Skill "kotlin-ktor-patterns" from https://github.com/affaan-m/ECC/blob/4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38/skills/kotlin-ktor-patterns/SKILL.md at commit 4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.

    Workflow

    What the source asks the agent to do

    1. 01

      kotlinx.serialization Setup

      Review the “kotlinx.serialization Setup” section in the pinned source before continuing.

      Review and apply the “kotlinx.serialization Setup” source section.
    2. 02

      When to Activate

      Building Ktor HTTP servers

      Building Ktor HTTP serversConfiguring Ktor plugins (Auth, CORS, ContentNegotiation, StatusPages)Implementing REST APIs with Ktor
    3. 03

      Application Structure

      Review the “Application Structure” section in the pinned source before continuing.

      Review and apply the “Application Structure” source section.
    4. 04

      Standard Ktor Project Layout

      Review the “Standard Ktor Project Layout” section in the pinned source before continuing.

      Review and apply the “Standard Ktor Project Layout” source section.
    5. 05

      Application Entry Point

      Review the “Application Entry Point” section in the pinned source before continuing.

      Review and apply the “Application Entry Point” source section.

    Permission review

    Static risk signals and limitations

    Network access

    medium · line 642

    The documentation includes network, browsing, or remote request actions.

    issuer: "https://example.com"

    Network access

    medium · line 643

    The documentation includes network, browsing, or remote request actions.

    audience: "https://example.com/api"

    Evidence record

    Why each signal appears

    EvidenceSourceComputedTestedEditorial
    SignalValueEvidence typeMeaning
    Quality score83/100ComputedDocumentation, specificity, maintenance, and trust rules
    Repository stars234,327SourceRepository attention, not individual Skill quality
    Compatibility0 platformsSourceDeclared in the catalog source record
    Usage guideautomated source guideEditorialGenerated or reviewed according to the visible evidence level

    Pinned source

    Provenance and original SKILL.md

    Repository
    affaan-m/ECC
    Skill path
    skills/kotlin-ktor-patterns/SKILL.md
    Commit
    4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38
    License
    MIT
    Collected
    2026-07-28
    Default branch
    main
    View the original SKILL.md

    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
    │   │   └── HealthRoutes.kt      # /health endpoints
    │   ├── models/
    │   │   ├── User.kt              # Domain models
    │   │   └── ApiResponse.kt       # Response envelopes
    │   ├── services/
    │   │   ├── UserService.kt       # Business logic
    │   │   └── AuthService.kt       # Auth logic
    │   ├── repositories/
    │   │   ├── UserRepository.kt    # Data access interface
    │   │   └── ExposedUserRepository.kt
    │   └── di/
    │       └── AppModule.kt         # Koin modules
    src/test/kotlin/
    ├── com/example/
    │   ├── routes/
    │   │   └── UserRoutesTest.kt
    │   └── services/
    │       └── UserServiceTest.kt
    

    Application Entry Point

    // Application.kt
    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

    // plugins/Routing.kt
    fun Application.configureRouting() {
        routing {
            userRoutes()
            authRoutes()
            healthRoutes()
        }
    }
    
    // routes/UserRoutes.kt
    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, "Missing id")
                val deleted = userService.delete(id)
                if (deleted) call.respond(HttpStatusCode.NoContent)
                else call.respond(HttpStatusCode.NotFound)
            }
        }
    }
    

    Route Organization with Authenticated Routes

    fun Route.userRoutes() {
        route("/users") {
            // Public routes
            get { /* list users */ }
            get("/{id}") { /* get user */ }
    
            // Protected routes
            authenticate("jwt") {
                post { /* create user - requires auth */ }
                put("/{id}") { /* update user - requires auth */ }
                delete("/{id}") { /* delete user - requires auth */ }
            }
        }
    }
    

    Content Negotiation & Serialization

    kotlinx.serialization Setup

    // plugins/Serialization.kt
    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<T>(
        val data: List<T>,
        val total: Long,
        val page: Int,
        val limit: Int,
    )
    

    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())
    }
    

    Authentication

    JWT Authentication

    // plugins/Authentication.kt
    fun Application.configureAuthentication() {
        val jwtSecret = environment.config.property("jwt.secret").getString()
        val jwtIssuer = environment.config.property("jwt.issuer").getString()
        val jwtAudience = environment.config.property("jwt.audience").getString()
        val jwtRealm = environment.config.property("jwt.realm").getString()
    
        install(Authentication) {
            jwt("jwt") {
                realm = jwtRealm
                verifier(
                    JWT.require(Algorithm.HMAC256(jwtSecret))
                        .withAudience(jwtAudience)
                        .withIssuer(jwtIssuer)
                        .build()
                )
                validate { credential ->
                    if (credential.payload.audience.contains(jwtAudience)) {
                        JWTPrincipal(credential.payload)
                    } else {
                        null
                    }
                }
                challenge { _, _ ->
                    call.respond(HttpStatusCode.Unauthorized, ApiResponse.error<Unit>("Invalid or expired token"))
                }
            }
        }
    }
    
    // Extracting user from JWT
    fun ApplicationCall.userId(): String =
        principal<JWTPrincipal>()
            ?.payload
            ?.getClaim("userId")
            ?.asString()
            ?: throw AuthenticationException("No userId in token")
    

    Auth Routes

    fun Route.authRoutes() {
        val authService by inject<AuthService>()
    
        route("/auth") {
            post("/login") {
                val request = call.receive<LoginRequest>()
                val token = authService.login(request.email, request.password)
                    ?: return@post call.respond(
                        HttpStatusCode.Unauthorized,
                        ApiResponse.error<Unit>("Invalid credentials"),
                    )
                call.respond(ApiResponse.ok(TokenResponse(token)))
            }
    
            post("/register") {
                val request = call.receive<RegisterRequest>()
                val user = authService.register(request)
                call.respond(HttpStatusCode.Created, ApiResponse.ok(user))
            }
    
            authenticate("jwt") {
                get("/me") {
                    val userId = call.userId()
                    val user = authService.getProfile(userId)
                    call.respond(ApiResponse.ok(user))
                }
            }
        }
    }
    

    Status Pages (Error Handling)

    // plugins/StatusPages.kt
    fun Application.configureStatusPages() {
        install(StatusPages) {
            exception<ContentTransformationException> { call, cause ->
                call.respond(
                    HttpStatusCode.BadRequest,
                    ApiResponse.error<Unit>("Invalid request body: ${cause.message}"),
                )
            }
    
            exception<IllegalArgumentException> { call, cause ->
                call.respond(
                    HttpStatusCode.BadRequest,
                    ApiResponse.error<Unit>(cause.message ?: "Bad request"),
                )
            }
    
            exception<AuthenticationException> { call, _ ->
                call.respond(
                    HttpStatusCode.Unauthorized,
                    ApiResponse.error<Unit>("Authentication required"),
                )
            }
    
            exception<AuthorizationException> { call, _ ->
                call.respond(
                    HttpStatusCode.Forbidden,
                    ApiResponse.error<Unit>("Access denied"),
                )
            }
    
            exception<NotFoundException> { call, cause ->
                call.respond(
                    HttpStatusCode.NotFound,
                    ApiResponse.error<Unit>(cause.message ?: "Resource not found"),
                )
            }
    
            exception<Throwable> { call, cause ->
                call.application.log.error("Unhandled exception", cause)
                call.respond(
                    HttpStatusCode.InternalServerError,
                    ApiResponse.error<Unit>("Internal server error"),
                )
            }
    
            status(HttpStatusCode.NotFound) { call, status ->
                call.respond(status, ApiResponse.error<Unit>("Route not found"))
            }
        }
    }
    

    CORS Configuration

    // plugins/CORS.kt
    fun Application.configureCORS() {
        install(CORS) {
            allowHost("localhost:3000")
            allowHost("example.com", schemes = listOf("https"))
            allowHeader(HttpHeaders.ContentType)
            allowHeader(HttpHeaders.Authorization)
            allowMethod(HttpMethod.Put)
            allowMethod(HttpMethod.Delete)
            allowMethod(HttpMethod.Patch)
            allowCredentials = true
            maxAgeInSeconds = 3600
        }
    }
    

    Koin Dependency Injection

    Module Definition

    // di/AppModule.kt
    val appModule = module {
        // Database
        single<Database> { DatabaseFactory.create(get()) }
    
        // Repositories
        single<UserRepository> { ExposedUserRepository(get()) }
        single<OrderRepository> { ExposedOrderRepository(get()) }
    
        // Services
        single { UserService(get()) }
        single { OrderService(get(), get()) }
        single { AuthService(get(), get()) }
    }
    
    // Application setup
    fun Application.configureDI() {
        install(Koin) {
            modules(appModule)
        }
    }
    

    Using Koin in Routes

    fun Route.userRoutes() {
        val userService by inject<UserService>()
    
        route("/users") {
            get {
                val users = userService.getAll()
                call.respond(ApiResponse.ok(users))
            }
        }
    }
    

    Koin for Testing

    class UserServiceTest : FunSpec(), KoinTest {
        override fun extensions() = listOf(KoinExtension(testModule))
    
        private val testModule = module {
            single<UserRepository> { mockk() }
            single { UserService(get()) }
        }
    
        private val repository by inject<UserRepository>()
        private val service by inject<UserService>()
    
        init {
            test("getUser returns user") {
                coEvery { repository.findById("1") } returns testUser
                service.getById("1") shouldBe testUser
            }
        }
    }
    

    Request Validation

    // Validate request data in routes
    fun Route.userRoutes() {
        val userService by inject<UserService>()
    
        post("/users") {
            val request = call.receive<CreateUserRequest>()
    
            // Validate
            require(request.name.isNotBlank()) { "Name is required" }
            require(request.name.length <= 100) { "Name must be 100 characters or less" }
            require(request.email.matches(Regex(".+@.+\\..+"))) { "Invalid email format" }
    
            val user = userService.create(request)
            call.respond(HttpStatusCode.Created, ApiResponse.ok(user))
        }
    }
    
    // Or use a validation extension
    fun CreateUserRequest.validate() {
        require(name.isNotBlank()) { "Name is required" }
        require(name.length <= 100) { "Name must be 100 characters or less" }
        require(email.matches(Regex(".+@.+\\..+"))) { "Invalid email format" }
    }
    

    WebSockets

    fun Application.configureWebSockets() {
        install(WebSockets) {
            pingPeriod = 15.seconds
            timeout = 15.seconds
            maxFrameSize = 64 * 1024 // 64 KiB — increase only if your protocol requires larger frames
            masking = false // Server-to-client frames are unmasked per RFC 6455; client-to-server are always masked by Ktor
        }
    }
    
    fun Route.chatRoutes() {
        val connections = Collections.synchronizedSet<Connection>(LinkedHashSet())
    
        webSocket("/chat") {
            val thisConnection = Connection(this)
            connections += thisConnection
    
            try {
                send("Connected! Users online: ${connections.size}")
    
                for (frame in incoming) {
                    frame as? Frame.Text ?: continue
                    val text = frame.readText()
                    val message = ChatMessage(thisConnection.name, text)
    
                    // Snapshot under lock to avoid ConcurrentModificationException
                    val snapshot = synchronized(connections) { connections.toList() }
                    snapshot.forEach { conn ->
                        conn.session.send(Json.encodeToString(message))
                    }
                }
            } catch (e: Exception) {
                logger.error("WebSocket error", e)
            } finally {
                connections -= thisConnection
            }
        }
    }
    
    data class Connection(val session: DefaultWebSocketSession) {
        val name: String = "User-${counter.getAndIncrement()}"
    
        companion object {
            private val counter = AtomicInteger(0)
        }
    }
    

    testApplication Testing

    Basic Route Testing

    class UserRoutesTest : FunSpec({
        test("GET /users returns list of users") {
            testApplication {
                application {
                    install(Koin) { modules(testModule) }
                    configureSerialization()
                    configureRouting()
                }
    
                val response = client.get("/users")
    
                response.status shouldBe HttpStatusCode.OK
                val body = response.body<ApiResponse<List<UserResponse>>>()
                body.success shouldBe true
                body.data.shouldNotBeNull().shouldNotBeEmpty()
            }
        }
    
        test("POST /users creates a user") {
            testApplication {
                application {
                    install(Koin) { modules(testModule) }
                    configureSerialization()
                    configureStatusPages()
                    configureRouting()
                }
    
                val client = createClient {
                    install(io.ktor.client.plugins.contentnegotiation.ContentNegotiation) {
                        json()
                    }
                }
    
                val response = client.post("/users") {
                    contentType(ContentType.Application.Json)
                    setBody(CreateUserRequest("Alice", "alice@example.com"))
                }
    
                response.status shouldBe HttpStatusCode.Created
            }
        }
    
        test("GET /users/{id} returns 404 for unknown id") {
            testApplication {
                application {
                    install(Koin) { modules(testModule) }
                    configureSerialization()
                    configureStatusPages()
                    configureRouting()
                }
    
                val response = client.get("/users/unknown-id")
    
                response.status shouldBe HttpStatusCode.NotFound
            }
        }
    })
    

    Testing Authenticated Routes

    class AuthenticatedRoutesTest : FunSpec({
        test("protected route requires JWT") {
            testApplication {
                application {
                    install(Koin) { modules(testModule) }
                    configureSerialization()
                    configureAuthentication()
                    configureRouting()
                }
    
                val response = client.post("/users") {
                    contentType(ContentType.Application.Json)
                    setBody(CreateUserRequest("Alice", "alice@example.com"))
                }
    
                response.status shouldBe HttpStatusCode.Unauthorized
            }
        }
    
        test("protected route succeeds with valid JWT") {
            testApplication {
                application {
                    install(Koin) { modules(testModule) }
                    configureSerialization()
                    configureAuthentication()
                    configureRouting()
                }
    
                val token = generateTestJWT(userId = "test-user")
    
                val client = createClient {
                    install(io.ktor.client.plugins.contentnegotiation.ContentNegotiation) { json() }
                }
    
                val response = client.post("/users") {
                    contentType(ContentType.Application.Json)
                    bearerAuth(token)
                    setBody(CreateUserRequest("Alice", "alice@example.com"))
                }
    
                response.status shouldBe HttpStatusCode.Created
            }
        }
    })
    

    Configuration

    application.yaml

    ktor:
      application:
        modules:
          - com.example.ApplicationKt.module
      deployment:
        port: 8080
    
    jwt:
      secret: ${JWT_SECRET}
      issuer: "https://example.com"
      audience: "https://example.com/api"
      realm: "example"
    
    database:
      url: ${DATABASE_URL}
      driver: "org.postgresql.Driver"
      maxPoolSize: 10
    

    Reading Config

    fun Application.configureDI() {
        val dbUrl = environment.config.property("database.url").getString()
        val dbDriver = environment.config.property("database.driver").getString()
        val maxPoolSize = environment.config.property("database.maxPoolSize").getString().toInt()
    
        install(Koin) {
            modules(module {
                single { DatabaseConfig(dbUrl, dbDriver, maxPoolSize) }
                single { DatabaseFactory.create(get()) }
            })
        }
    }
    

    Quick Reference: Ktor Patterns

    PatternDescription
    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.

    Alternatives

    Compare before choosing