O Ktor e o framework web oficial da JetBrains, construido inteiramente em Kotlin e projetado para aproveitar ao maximo os recursos da linguagem, especialmente coroutines. Diferente do Spring Boot, que traz um ecossistema completo e opinado, o Ktor segue uma filosofia minimalista e modular: voce instala apenas os plugins que precisa, mantendo a aplicacao leve e o startup rapido. Isso o torna ideal para microservicos, APIs e aplicacoes que exigem alta performance com baixo consumo de recursos. Neste guia, vamos construir uma API completa com Ktor, cobrindo rotas, serializacao, banco de dados, autenticacao e deploy.

Configurando o Projeto

O Ktor Project Generator (start.ktor.io) cria a estrutura inicial. A configuracao no build.gradle.kts:

plugins {
    kotlin("jvm") version "1.9.22"
    id("io.ktor.plugin") version "2.3.7"
    kotlin("plugin.serialization") version "1.9.22"
}

application {
    mainClass.set("com.exemplo.ApplicationKt")
}

dependencies {
    // Core
    implementation("io.ktor:ktor-server-core-jvm")
    implementation("io.ktor:ktor-server-netty-jvm")

    // Plugins
    implementation("io.ktor:ktor-server-content-negotiation-jvm")
    implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
    implementation("io.ktor:ktor-server-status-pages-jvm")
    implementation("io.ktor:ktor-server-auth-jvm")
    implementation("io.ktor:ktor-server-auth-jwt-jvm")
    implementation("io.ktor:ktor-server-cors-jvm")

    // Banco de dados
    implementation("org.jetbrains.exposed:exposed-core:0.46.0")
    implementation("org.jetbrains.exposed:exposed-dao:0.46.0")
    implementation("org.jetbrains.exposed:exposed-jdbc:0.46.0")
    implementation("org.jetbrains.exposed:exposed-java-time:0.46.0")
    implementation("org.postgresql:postgresql:42.7.1")
    implementation("com.zaxxer:HikariCP:5.1.0")

    // Logging
    implementation("ch.qos.logback:logback-classic:1.4.14")

    // Testes
    testImplementation("io.ktor:ktor-server-tests-jvm")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
}

Estrutura da Aplicacao

O Ktor usa uma funcao main simples para iniciar o servidor:

fun main() {
    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
        configurarPlugins()
        configurarRoteamento()
        configurarBancoDeDados()
    }.start(wait = true)
}

// Ou usando application.conf (HOCON)
fun main(args: Array<String>) {
    io.ktor.server.netty.EngineMain.main(args)
}

fun Application.module() {
    configurarPlugins()
    configurarRoteamento()
    configurarBancoDeDados()
}

Configurando Plugins

Os plugins adicionam funcionalidades ao Ktor de forma modular:

fun Application.configurarPlugins() {
    install(ContentNegotiation) {
        json(Json {
            prettyPrint = true
            isLenient = true
            ignoreUnknownKeys = true
            encodeDefaults = true
        })
    }

    install(StatusPages) {
        exception<RecursoNaoEncontradoException> { call, causa ->
            call.respond(
                HttpStatusCode.NotFound,
                ErroResponse(404, causa.message ?: "Nao encontrado")
            )
        }
        exception<ValidacaoException> { call, causa ->
            call.respond(
                HttpStatusCode.BadRequest,
                ErroResponse(400, causa.message ?: "Dados invalidos")
            )
        }
        exception<Throwable> { call, causa ->
            call.application.environment.log.error("Erro interno", causa)
            call.respond(
                HttpStatusCode.InternalServerError,
                ErroResponse(500, "Erro interno do servidor")
            )
        }
    }

    install(CORS) {
        anyHost()
        allowMethod(HttpMethod.Options)
        allowMethod(HttpMethod.Put)
        allowMethod(HttpMethod.Delete)
        allowHeader(HttpHeaders.ContentType)
        allowHeader(HttpHeaders.Authorization)
    }
}

Definindo Rotas

O roteamento no Ktor e declarativo e baseado em DSL:

fun Application.configurarRoteamento() {
    routing {
        route("/api") {
            produtoRoutes()
            categoriaRoutes()
        }
    }
}

fun Route.produtoRoutes() {
    val service = ProdutoService()

    route("/produtos") {
        get {
            val pagina = call.request.queryParameters["pagina"]?.toIntOrNull() ?: 0
            val tamanho = call.request.queryParameters["tamanho"]?.toIntOrNull() ?: 20
            val produtos = service.listarTodos(pagina, tamanho)
            call.respond(produtos)
        }

        get("/{id}") {
            val id = call.parameters["id"]?.toLongOrNull()
                ?: throw ValidacaoException("ID invalido")
            val produto = service.buscarPorId(id)
            call.respond(produto)
        }

        post {
            val request = call.receive<CriarProdutoRequest>()
            val produto = service.criar(request)
            call.respond(HttpStatusCode.Created, produto)
        }

        put("/{id}") {
            val id = call.parameters["id"]?.toLongOrNull()
                ?: throw ValidacaoException("ID invalido")
            val request = call.receive<AtualizarProdutoRequest>()
            val produto = service.atualizar(id, request)
            call.respond(produto)
        }

        delete("/{id}") {
            val id = call.parameters["id"]?.toLongOrNull()
                ?: throw ValidacaoException("ID invalido")
            service.deletar(id)
            call.respond(HttpStatusCode.NoContent)
        }
    }
}

Banco de Dados com Exposed

Exposed e o framework ORM da JetBrains para Kotlin, com integracao natural com Ktor:

// Definicao da tabela
object Produtos : LongIdTable("produtos") {
    val nome = varchar("nome", 255)
    val preco = decimal("preco", 10, 2)
    val descricao = text("descricao").nullable()
    val criadoEm = datetime("criado_em").defaultExpression(CurrentDateTime)
    val categoriaId = reference("categoria_id", Categorias).nullable()
}

// Configuracao do banco
fun Application.configurarBancoDeDados() {
    val config = HikariConfig().apply {
        driverClassName = "org.postgresql.Driver"
        jdbcUrl = environment.config.property("database.url").getString()
        username = environment.config.property("database.user").getString()
        password = environment.config.property("database.password").getString()
        maximumPoolSize = 10
    }

    val dataSource = HikariDataSource(config)
    Database.connect(dataSource)

    transaction {
        SchemaUtils.create(Produtos, Categorias)
    }
}

Camada de Servico

class ProdutoService {

    suspend fun listarTodos(pagina: Int, tamanho: Int): List<ProdutoResponse> {
        return dbQuery {
            Produtos.selectAll()
                .limit(tamanho, offset = (pagina * tamanho).toLong())
                .map { it.toProdutoResponse() }
        }
    }

    suspend fun buscarPorId(id: Long): ProdutoResponse {
        return dbQuery {
            Produtos.selectAll().where { Produtos.id eq id }
                .singleOrNull()
                ?.toProdutoResponse()
                ?: throw RecursoNaoEncontradoException("Produto $id nao encontrado")
        }
    }

    suspend fun criar(request: CriarProdutoRequest): ProdutoResponse {
        validar(request)
        return dbQuery {
            val id = Produtos.insertAndGetId {
                it[nome] = request.nome
                it[preco] = request.preco
                it[descricao] = request.descricao
                it[categoriaId] = request.categoriaId
            }
            buscarPorIdInterno(id.value)
        }
    }

    suspend fun deletar(id: Long) {
        dbQuery {
            val linhasAfetadas = Produtos.deleteWhere { Produtos.id eq id }
            if (linhasAfetadas == 0) {
                throw RecursoNaoEncontradoException("Produto $id nao encontrado")
            }
        }
    }

    private fun validar(request: CriarProdutoRequest) {
        if (request.nome.isBlank()) {
            throw ValidacaoException("Nome e obrigatorio")
        }
        if (request.preco <= BigDecimal.ZERO) {
            throw ValidacaoException("Preco deve ser positivo")
        }
    }
}

// Funcao utilitaria para transacoes assincronas
suspend fun <T> dbQuery(bloco: () -> T): T {
    return newSuspendedTransaction(Dispatchers.IO) {
        bloco()
    }
}

Autenticacao com JWT

fun Application.configurarAutenticacao() {
    val segredo = environment.config.property("jwt.secret").getString()
    val emissor = environment.config.property("jwt.issuer").getString()
    val audiencia = environment.config.property("jwt.audience").getString()

    install(Authentication) {
        jwt("auth-jwt") {
            realm = "Meu App"
            verifier(
                JWT.require(Algorithm.HMAC256(segredo))
                    .withIssuer(emissor)
                    .withAudience(audiencia)
                    .build()
            )
            validate { credential ->
                if (credential.payload.audience.contains(audiencia)) {
                    JWTPrincipal(credential.payload)
                } else null
            }
            challenge { _, _ ->
                call.respond(
                    HttpStatusCode.Unauthorized,
                    ErroResponse(401, "Token invalido ou expirado")
                )
            }
        }
    }
}

// Rotas protegidas
fun Route.rotasProtegidas() {
    authenticate("auth-jwt") {
        get("/api/perfil") {
            val principal = call.principal<JWTPrincipal>()
            val usuarioId = principal!!.payload.getClaim("usuario_id").asLong()
            val usuario = usuarioService.buscarPorId(usuarioId)
            call.respond(usuario)
        }
    }
}

// Gerar token
fun gerarToken(usuario: Usuario): String {
    return JWT.create()
        .withIssuer(emissor)
        .withAudience(audiencia)
        .withClaim("usuario_id", usuario.id)
        .withClaim("email", usuario.email)
        .withExpiresAt(Date(System.currentTimeMillis() + 3600000))
        .sign(Algorithm.HMAC256(segredo))
}

Boas Praticas com Ktor

  • Organize rotas em extension functions: cada recurso deve ter seu proprio arquivo de rotas como extension function de Route.
  • Use injecao de dependencia: Koin integra nativamente com Ktor e facilita testes.
  • Valide entrada manualmente ou com bibliotecas: Ktor nao tem validacao embutida como Spring. Use funcoes de validacao explicitas.
  • Configure logging adequadamente: o Logback e o padrao, configure niveis por pacote para debug eficiente.
  • Aproveite coroutines: todas as rotas do Ktor ja sao funcoes suspensas. Use Dispatchers.IO para operacoes bloqueantes.
  • Use configuracao externalizada: o arquivo application.conf permite configuracoes diferentes por ambiente.

Erros Comuns e Armadilhas

  • Esquecer o plugin de serializacao: sem ContentNegotiation configurado, o Ktor nao serializa/deserializa objetos automaticamente. Voce recebera erros ao usar call.receive ou call.respond.
  • Bloquear a thread do servidor: operacoes bloqueantes sem withContext(Dispatchers.IO) travam o event loop do Netty.
  • Nao tratar excecoes: sem StatusPages, excecoes nao tratadas retornam respostas HTML genericas em vez de JSON.
  • Ignorar CORS: clientes web receberao erros se CORS nao estiver configurado corretamente.
  • Conexoes de banco nao gerenciadas: sempre use um pool de conexoes como HikariCP em vez de conexoes diretas.

Conclusao e Proximos Passos

O Ktor oferece uma alternativa leve e idiomatica ao Spring Boot para desenvolvimento backend em Kotlin. Sua natureza modular, suporte nativo a coroutines e API baseada em DSL resultam em aplicacoes performaticas e faceis de entender. Para ir alem, explore WebSockets com Ktor para comunicacao em tempo real, estude o deploy com Docker e confira nossos guias sobre microservicos e CI/CD para completar sua stack de backend com Kotlin.