Communitygithub.com

Padrões Ktor Server

Padrões de servidor Ktor incluindo DSL de roteamento, plugins, autenticação, injeção de dependência com Koin, serialização kotlinx, WebSockets e testes com testApplication. Utilize ao construir um servidor Ktor — roteamento, plugins, autenticação, DI, serialização ou testes.

O que é Padrões Ktor Server?

Este skill fornece padrões abrangentes e exemplos práticos para construir servidores HTTP robustos e maintaináveis usando o framework Ktor, com foco em corotinas Kotlin. Ele cobre tópicos essenciais como DSL de roteamento, organização de projetos, plugins comuns (ContentNegotiation, CORS, StatusPages), autenticação JWT, injeção de dependência com Koin, WebSockets e testes de integração com testApplication. O objetivo é ajudar desenvolvedores a configurarem rapidamente um servidor Ktor seguindo melhores práticas, evitando erros comuns e garantindo escalabilidade. É compatível com a versão mais recente do Ktor (3.x) e integra-se com a biblioteca kotlinx.serialization para serialização JSON eficiente e segura.

Funciona com~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/kotlin-ktor-patterns

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Padrões de Servidor Ktor

Padrões abrangentes para construir servidores HTTP robustos e maintaináveis com Kotlin e corotinas.

Quando Ativar

  • Construindo servidores HTTP Ktor
  • Configurando plugins Ktor (Auth, CORS, ContentNegotiation, StatusPages)
  • Implementando APIs REST com Ktor
  • Configurando injeção de dependência com Koin
  • Escrevendo testes de integração Ktor com testApplication
  • Trabalhando com WebSockets no Ktor

Estrutura da Aplicação

Layout Padrão de Projeto Ktor

src/main/kotlin/
├── com/example/
│   ├── Application.kt           # Ponto de entrada, configuração do módulo
│   ├── plugins/
│   │   ├── Routing.kt           # Definições de rotas
│   │   ├── Serialization.kt     # Configuração de negociação de conteúdo
│   │   ├── Authentication.kt    # Configuração de autenticação
│   │   ├── StatusPages.kt       # Tratamento de erros
│   │   └── CORS.kt              # Configuração CORS
│   ├── routes/
│   │   ├── UserRoutes.kt        # Endpoints /users
│   │   ├── AuthRoutes.kt        # Endpoints /auth
│   │   └── HealthRoutes.kt      # Endpoints /health
│   ├── models/
│   │   ├── User.kt              # Modelos de domínio
│   │   └── ApiResponse.kt       # Envelopes de resposta
│   ├── services/
│   │   ├── UserService.kt       # Lógica de negócio
│   │   └── AuthService.kt       # Lógica de autenticação
│   ├── repositories/
│   │   ├── UserRepository.kt    # Interface de acesso a dados
│   │   └── ExposedUserRepository.kt
│   └── di/
│       └── AppModule.kt         # Módulos Koin
src/test/kotlin/
├── com/example/
│   ├── routes/
│   │   └── UserRoutesTest.kt
│   └── services/
│       └── UserServiceTest.kt

Ponto de Entrada da Aplicação

// Application.kt
fun main() {
    embeddedServer(Netty, port = 8080, module = Application::module).start(wait = true)
}

fun Application.module() {
    configureSerialization()
    configureAuthentication()
    configureStatusPages()
    configureCORS()
    configureDI()
    configureRouting()
}

DSL de Roteamento

Rotas Básicas

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

Organização de Rotas com Rotas Autenticadas

fun Route.userRoutes() {
    route("/users") {
        // Rotas públicas
        get { /* listar usuários */ }
        get("/{id}") { /* obter usuário */ }

        // Rotas protegidas
        authenticate("jwt") {
            post { /* criar usuário - requer autenticação */ }
            put("/{id}") { /* atualizar usuário - requer autenticação */ }
            delete("/{id}") { /* deletar usuário - requer autenticação */ }
        }
    }
}

Negociação de Conteúdo e Serialização

Configuração kotlinx.serialization

// plugins/Serialization.kt
fun Application.configureSerialization() {
    install(ContentNegotiation) {
        json(Json {
            prettyPrint = true
            isLenient = false
            ignoreUnknownKeys = true
            encodeDefaults = true
            explicitNulls = false
        })
    }
}

Modelos Serializáveis

@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,
)

Serializadores Personalizados

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

Autenticação

Autenticação JWT

// 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>("Token inválido ou expirado"))
            }
        }
    }
}

// Extraindo usuário do JWT
fun ApplicationCall.userId(): String =
    principal<JWTPrincipal>()
        ?.payload
        ?.getClaim("userId")
        ?.asString()
        ?: throw AuthenticationException("Nenhum userId no token")

Rotas de Autenticação

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>("Credenciais inválidas"),
                )
            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))
            }
        }
    }
}

Páginas de Status (Tratamento de Erros)

// plugins/StatusPages.kt
fun Application.configureStatusPages() {
    install(StatusPages) {
        exception<ContentTransformationException> { call, cause ->
            call.respond(
                HttpStatusCode.BadRequest,
                ApiResponse.error<Unit>("Corpo da requisição inválido: ${cause.message}"),
            )
        }

        exception<IllegalArgumentException> { call, cause ->
            call.respond(
                HttpStatusCode.BadRequest,
                ApiResponse.error<Unit>(cause.message ?: "Requisição inválida"),
            )
        }

        exception<AuthenticationException> { call, _ ->
            call.respond(
                HttpStatusCode.Unauthorized,
                ApiResponse.error<Unit>("Autenticação necessária"),
            )
        }

        exception<AuthorizationException> { call, _ ->
            call.respond(
                HttpStatusCode.Forbidden,
                ApiResponse.error<Unit>("Acesso negado"),
            )
        }

        exception<NotFoundException> { call, cause ->
            call.respond(
                HttpStatusCode.NotFound,
                ApiResponse.error<Unit>(cause.message ?: "Recurso não encontrado"),
            )
        }

        exception<Throwable> { call, cause ->
            call.application.log.error("Exceção não tratada", cause)
            call.respond(
                HttpStatusCode.InternalServerError,
                ApiResponse.error<Unit>("Erro interno do servidor"),
            )
        }

        status(HttpStatusCode.NotFound) { call, status ->
            call.respond(status, ApiResponse.error<Unit>("Rota não encontrada"))
        }
    }
}

Configuração CORS

// 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
    }
}

Injeção de Dependência com Koin

Definição do Módulo

// di/AppModule.kt
val appModule = module {
    // Banco de dados
    single<Database> { DatabaseFactory.create(get()) }

    // Repositórios
    single<UserRepository> { ExposedUserRepository(get()) }
    single<OrderRepository> { ExposedOrderRepository(get()) }

    // Serviços
    single { UserService(get()) }
    single { OrderService(get(), get()) }
    single { AuthService(get(), get()) }
}

// Configuração da aplicação
fun Application.configureDI() {
    install(Koin) {
        modules(appModule)
    }
}

Usando Koin nas Rotas

fun Route.userRoutes() {
    val userService by inject<UserService>()

    route("/users") {
        get {
            val users = userService.getAll()
            call.respond(ApiResponse.ok(users))
        }
    }
}

Koin para Testes

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 retorna usuário") {
            coEvery { repository.findById("1") } returns testUser
            service.getById("1") shouldBe testUser
        }
    }
}

Validação de Requisições

// Validar dados da requisição nas rotas
fun Route.userRoutes() {
    val userService by inject<UserService>()

    post("/users") {
        val request = call.receive<CreateUserRequest>()

        // Validar
        require(request.name.isNotBlank()) { "Nome é obrigatório" }
        require(request.name.length <= 100) { "Nome deve ter 100 caracteres ou menos" }
        require(request.email.matches(Regex(".+@.+\\..+"))) { "Formato de email inválido" }

        val user = userService.create(request)
        call.respond(HttpStatusCode.Created, ApiResponse.ok(user))
    }
}

// Ou usar uma extensão de validação
fun CreateUserRequest.validate() {
    require(name.isNotBlank()) { "Nome é obrigatório" }
    require(name.length <= 100) { "Nome deve ter 100 caracteres ou menos" }
    require(email.matches(Regex(".+@.+\\..+"))) { "Formato de email inválido" }
}

WebSockets

fun Application.configureWebSockets() {
    install(WebSockets) {
        pingPeriod = 15.seconds
        timeout = 15.seconds
        maxFrameSize = 64 * 1024 // 64 KiB — aumente apenas se seu protocolo exigir quadros maiores
        masking = false // Quadros servidor-para-cliente são desmascarados conforme RFC 6455; cliente-para-servidor são sempre mascarados pelo Ktor
    }
}

fun Route.chatRoutes() {
    val connections = Collections.synchronizedSet<Connection>(LinkedHashSet())

    webSocket("/chat") {
        val thisConnection = Connection(this)
        connections += thisConnection

        try {
            send("Conectado! Usuários online: ${connections.size}")

            for (frame in incoming) {
                frame as? Frame.Text ?: continue
                val text = frame.readText()
                val message = ChatMessage(thisConnection.name, text)

                // Snapshot sob bloqueio para evitar ConcurrentModificationException
                val snapshot = synchronized(connections) { connections.toList() }
                snapshot.forEach { conn ->
                    conn.session.send(Json.encodeToString(message))
                }
            }
        } catch (e: Exception) {
            logger.error("Erro no WebSocket", e)
        } finally {
            connections -= thisConnection
        }
    }
}

data class Connection(val session: DefaultWebSocketSession) {
    val name: String = "User-${counter.getAndIncrement()}"

    companion object {
        private val counter = AtomicInteger(1)
    }
}

@Serializable
data class ChatMessage(
    val sender: String,
    val text: String,
    @Serializable(with = InstantSerializer::class)
    val timestamp: Instant = Clock.System.now(),
)

Testes de Integração

Teste básico com testApplication

// src/test/kotlin/com/example/routes/UserRoutesTest.kt
class UserRoutesTest {
    @Test
    fun `test get users endpoint`() = testApplication {
        application {
            module()
        }

        client.get("/users").apply {
            assertEquals(HttpStatusCode.OK, status)
        }
    }

    @Test
    fun `test create user endpoint`() = testApplication {
        application {
            module()
        }

        val request = CreateUserRequest("John Doe", "[email protected]")
        client.post("/users") {
            contentType(ContentType.Application.Json)
            setBody(request)
        }.apply {
            assertEquals(HttpStatusCode.Created, status)
        }
    }
}

Teste com injeção de dependência e mock

class UserRoutesTest : KoinTest {
    private val userRepository = mockk<UserRepository>()

    private val testModule = module {
        single<UserRepository> { userRepository }
        single { UserService(get()) }
    }

    @Test
    fun `test get user by id`() = testApplication {
        environment {
            config = MapApplicationConfig(
                "jwt.secret" to "test-secret",
                "jwt.issuer" to "test",
                "jwt.audience" to "test",
                "jwt.realm" to "test",
            )
        }

        application {
            modules(testModule)
            configureSerialization()
            configureRouting()
        }

        coEvery { userRepository.findById("1") } returns testUser

        client.get("/users/1").apply {
            assertEquals(HttpStatusCode.OK, status)
            val response = body<UserResponse>()
            assertEquals("John Doe", response.name)
        }
    }
}

Teste de autenticação

@Test
fun `test protected endpoint without auth`() = testApplication {
    application {
        modules(testModule)
        configureSerialization()
        configureAuthentication()
        configureRouting()
    }

    client.post("/users") {
        contentType(ContentType.Application.Json)
        setBody(CreateUserRequest("John", "[email protected]"))
    }.apply {
        assertEquals(HttpStatusCode.Unauthorized, status)
    }
}

@Test
fun `test protected endpoint with valid auth`() = testApplication {
    application {
        modules(testModule)
        configureSerialization()
        configureAuthentication()
        configureRouting()
    }

    val token = generateTestToken("1", "test-secret")

    client.post("/users") {
        contentType(ContentType.Application.Json)
        bearerAuth(token)
        setBody(CreateUserRequest("John", "[email protected]"))
    }.apply {
        assertEquals(HttpStatusCode.Created, status)
    }
}

Configuração de Ambiente

HOCON (application.conf)

ktor {
    deployment {
        port = 8080
        port = ${?PORT}
    }
    application {
        modules = [ com.example.ApplicationKt.module ]
    }
}

jwt {
    secret = "minha-chave-secreta"
    secret = ${?JWT_SECRET}
    issuer = "myapp"
    issuer = ${?JWT_ISSUER}
    audience = "myapp-users"
    audience = ${?JWT_AUDIENCE}
    realm = "myapp"
    realm = ${?JWT_REALM}
}

Variáveis de Ambiente com dotenv ou acesso programático

// Carregar variáveis de ambiente usando o config embutido do Ktor
val jwtSecret = environment.config.property("jwt.secret").getString()

// Ou usando uma biblioteca como ktor-server-config-yaml

Tratamento Avançado de Erros e Validação

Resposta de Erro Estruturada

@Serializable
data class ValidationError(
    val field: String,
    val message: String,
)

@Serializable
data class ErrorResponse(
    val success: Boolean = false,
    val message: String,
    val errors: List<ValidationError>? = null,
)

// Usar no controlador
fun Route.userRoutes() {
    val userService by inject<UserService>()

    post("/users") {
        val request = call.receive<CreateUserRequest>()
        val errors = mutableListOf<ValidationError>()

        if (request.name.isBlank()) {
            errors.add(ValidationError("name", "Nome é obrigatório"))
        }
        if (!request.email.matches(Regex(".+@.+\\..+"))) {
            errors.add(ValidationError("email", "Formato de email inválido"))
        }

        if (errors.isNotEmpty()) {
            return@post call.respond(HttpStatusCode.BadRequest, ErrorResponse(
                message = "Falha na validação",
                errors = errors,
            ))
        }

        val user = userService.create(request)
        call.respond(HttpStatusCode.Created, ApiResponse.ok(user))
    }
}

Geração de Resposta de Sucesso Padronizada

// Resposta de sucesso padronizada
fun <T> ApplicationCall.respondOk(data: T) =
    respond(ApiResponse.ok(data))

fun <T> ApplicationCall.respondCreated(data: T) =
    respond(HttpStatusCode.Created, ApiResponse.ok(data))

fun ApplicationCall.respondNoContent() =
    respond(HttpStatusCode.NoContent)

// Uso
fun Route.userRoutes() {
    val userService by inject<UserService>()

    route("/users") {
        get {
            val users = userService.getAll()
            call.respondOk(users)
        }

        post {
            val request = call.receive<CreateUserRequest>()
            val user = userService.create(request)
            call.respondCreated(user)
        }

        delete("/{id}") {
            val id = call.parameters["id"]
                ?: return@delete call.respond(HttpStatusCode.BadRequest, "Missing id")
            val deleted = userService.delete(id)
            if (deleted) call.respondNoContent()
            else call.respond(HttpStatusCode.NotFound)
        }
    }
}

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

Habilidades Relacionadas