Se você procura um framework leve, idiomático e 100% Kotlin para construir APIs REST, o Ktor é a escolha certa. Criado pela JetBrains (a mesma empresa por trás do Kotlin), o Ktor aproveita o melhor da linguagem: coroutines nativas, DSLs elegantes e tipagem forte. Neste tutorial, vamos construir uma API REST completa do zero.

O que é o Ktor?

Ktor é um framework assíncrono para criar aplicações conectadas — servidores HTTP, clientes HTTP, microserviços e WebSockets. Diferente do Spring Boot, que traz um ecossistema gigante de convenções e auto-configuração, o Ktor segue a filosofia de ser minimalista e modular: você adiciona apenas o que precisa, sem mágica escondida.

As principais características do Ktor incluem:

  • Assincronicidade nativa: construído sobre coroutines, cada requisição é uma coroutine — sem threads bloqueadas
  • DSL idiomático: a configuração do servidor usa DSLs de Kotlin, resultando em código expressivo e legível
  • Multiplataforma: o cliente HTTP do Ktor roda em JVM, Native, JavaScript e WebAssembly
  • Modular: sistema de plugins onde você instala só o que precisa (serialização, autenticação, CORS, etc.)
  • Transparente: sem anotações mágicas, sem injeção de dependência implícita, sem proxy de classes

Configurando o projeto

A maneira mais rápida de iniciar é pelo Ktor Project Generator, onde você seleciona os plugins desejados e baixa o projeto pronto. Alternativamente, configure manualmente com Gradle:

// build.gradle.kts
plugins {
    kotlin("jvm") version "2.1.0"
    kotlin("plugin.serialization") version "2.1.0"
    id("io.ktor.plugin") version "3.1.1"
}

dependencies {
    implementation("io.ktor:ktor-server-core")
    implementation("io.ktor:ktor-server-netty")
    implementation("io.ktor:ktor-server-content-negotiation")
    implementation("io.ktor:ktor-serialization-kotlinx-json")
    implementation("io.ktor:ktor-server-status-pages")
    implementation("ch.qos.logback:logback-classic:1.5.15")

    testImplementation("io.ktor:ktor-server-test-host")
    testImplementation("org.jetbrains.kotlin:kotlin-test")
}

O plugin io.ktor.plugin facilita o build e gera o fat JAR para deploy automaticamente.

Hello World com Ktor

Vamos criar o servidor mais simples possível:

import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun main() {
    embeddedServer(Netty, port = 8080) {
        routing {
            get("/") {
                call.respondText("Olá, Ktor!")
            }
        }
    }.start(wait = true)
}

Execute e acesse http://localhost:8080 — pronto, seu servidor Ktor está rodando. Note como tudo é explícito: você vê o engine (Netty), a porta, as rotas. Nada acontece sem que você configure.

Roteamento com a DSL do Ktor

O sistema de roteamento do Ktor usa uma DSL expressiva que suporta agrupamento, parâmetros de caminho e todos os métodos HTTP:

fun Application.configureRouting() {
    routing {
        route("/api/v1") {
            get("/tarefas") {
                // Listar todas as tarefas
            }

            get("/tarefas/{id}") {
                val id = call.parameters["id"]?.toIntOrNull()
                    ?: return@get call.respondText(
                        "ID inválido",
                        status = HttpStatusCode.BadRequest
                    )
                // Buscar tarefa por ID
            }

            post("/tarefas") {
                // Criar nova tarefa
            }

            put("/tarefas/{id}") {
                // Atualizar tarefa existente
            }

            delete("/tarefas/{id}") {
                // Remover tarefa
            }
        }
    }
}

A função route("/api/v1") agrupa todas as rotas sob um prefixo comum, evitando repetição. Os parâmetros de caminho como {id} são acessados via call.parameters.

Serialização JSON com kotlinx.serialization

Para trabalhar com JSON, o Ktor se integra nativamente com o kotlinx.serialization. Primeiro, instale o plugin de Content Negotiation:

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

Agora defina seus modelos de dados com a anotação @Serializable:

import kotlinx.serialization.Serializable

@Serializable
data class Tarefa(
    val id: Int = 0,
    val titulo: String,
    val descricao: String = "",
    val concluida: Boolean = false
)

@Serializable
data class TarefaRequest(
    val titulo: String,
    val descricao: String = ""
)

Exemplo completo: API CRUD de tarefas

Agora vamos juntar tudo em uma API CRUD funcional. Usaremos uma lista em memória para simplificar (em produção, você conectaria a um banco de dados com Exposed ou ktorm):

import io.ktor.http.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import java.util.concurrent.atomic.AtomicInteger

val tarefas = mutableListOf<Tarefa>()
val idCounter = AtomicInteger(0)

fun Application.configureTarefasRoutes() {
    routing {
        route("/api/tarefas") {

            // GET — listar todas
            get {
                call.respond(tarefas)
            }

            // GET — buscar por ID
            get("/{id}") {
                val id = call.parameters["id"]?.toIntOrNull()
                val tarefa = tarefas.find { it.id == id }
                if (tarefa == null) {
                    call.respond(HttpStatusCode.NotFound, "Tarefa não encontrada")
                } else {
                    call.respond(tarefa)
                }
            }

            // POST — criar nova
            post {
                val request = call.receive<TarefaRequest>()
                val novaTarefa = Tarefa(
                    id = idCounter.incrementAndGet(),
                    titulo = request.titulo,
                    descricao = request.descricao
                )
                tarefas.add(novaTarefa)
                call.respond(HttpStatusCode.Created, novaTarefa)
            }

            // PUT — atualizar existente
            put("/{id}") {
                val id = call.parameters["id"]?.toIntOrNull()
                val index = tarefas.indexOfFirst { it.id == id }
                if (index == -1) {
                    call.respond(HttpStatusCode.NotFound, "Tarefa não encontrada")
                } else {
                    val request = call.receive<TarefaRequest>()
                    val atualizada = tarefas[index].copy(
                        titulo = request.titulo,
                        descricao = request.descricao
                    )
                    tarefas[index] = atualizada
                    call.respond(atualizada)
                }
            }

            // DELETE — remover
            delete("/{id}") {
                val id = call.parameters["id"]?.toIntOrNull()
                val removido = tarefas.removeAll { it.id == id }
                if (removido) {
                    call.respond(HttpStatusCode.NoContent)
                } else {
                    call.respond(HttpStatusCode.NotFound, "Tarefa não encontrada")
                }
            }
        }
    }
}

Plugins essenciais

O Ktor funciona com um sistema de plugins que você instala conforme a necessidade. Além do ContentNegotiation que já vimos, os mais importantes são:

StatusPages — tratamento centralizado de erros:

install(StatusPages) {
    exception<IllegalArgumentException> { call, cause ->
        call.respond(HttpStatusCode.BadRequest, cause.localizedMessage)
    }
    exception<Throwable> { call, cause ->
        call.respond(
            HttpStatusCode.InternalServerError,
            "Erro interno: ${cause.localizedMessage}"
        )
    }
}

CORS — controle de acesso cross-origin:

install(CORS) {
    allowHost("meusite.com.br")
    allowHeader(HttpHeaders.ContentType)
    allowMethod(HttpMethod.Put)
    allowMethod(HttpMethod.Delete)
}

CallLogging — logs estruturados de cada requisição:

install(CallLogging) {
    level = Level.INFO
    filter { call -> call.request.path().startsWith("/api") }
}

Outros plugins populares incluem Authentication (JWT, OAuth, Session), WebSockets, Rate Limiting e Compression.

Ktor vs Spring Boot: quando usar cada um?

Essa e uma das perguntas mais comuns entre devs Kotlin no backend. A resposta depende do contexto:

CritérioKtorSpring Boot
FilosofiaMinimalista, explícitoConvenção sobre configuração
Curva de aprendizadoMais simples para quem sabe KotlinRequer conhecer o ecossistema Spring
PerformanceMais leve, menos overheadMais pesado, mas altamente otimizado
EcossistemaMenor, mas crescendoGigantesco, maduro
MultiplataformaSim (cliente HTTP)Apenas JVM
Ideal paraMicroserviços, APIs leves, serverlessAplicações enterprise, monolitos

Se você vem do mundo Java e já conhece Spring, a combinação Kotlin + Spring Boot oferece produtividade imediata. Se quer algo 100% Kotlin, sem heranças do mundo Java, o Ktor é o caminho natural. Para deploy serverless na AWS Lambda, o Ktor se destaca pelo cold start mais rápido.

Deploy: fat JAR para produção

O plugin do Ktor gera um fat JAR automaticamente. Basta rodar:

./gradlew buildFatJar

O JAR fica em build/libs/seu-projeto-all.jar. Para executar em produção:

java -jar build/libs/seu-projeto-all.jar

Para containerizar com Docker, um Dockerfile simples funciona:

FROM eclipse-temurin:21-jre-alpine
COPY build/libs/seu-projeto-all.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

O Ktor também suporta GraalVM Native Image para builds nativos com cold start de milissegundos — ideal para ambientes Kubernetes e serverless.

Testando a API

Ktor tem um módulo de testes embutido que permite testar rotas sem subir o servidor de verdade:

@Test
fun `deve criar tarefa com sucesso`() = testApplication {
    application {
        configureSerialization()
        configureTarefasRoutes()
    }

    val response = client.post("/api/tarefas") {
        contentType(ContentType.Application.Json)
        setBody("""{"titulo": "Estudar Ktor", "descricao": "Completar tutorial"}""")
    }

    assertEquals(HttpStatusCode.Created, response.status)
}

O testApplication cria um ambiente de teste completo sem abrir portas de rede, tornando os testes rapidos e isolados.

Conclusao

O Ktor e um framework poderoso que abraca a filosofia do Kotlin: conciso, seguro e explícito. Ele é perfeito para microserviços, APIs REST e aplicações onde você quer controle total sobre o que acontece. Combinado com coroutines para concorrência e kotlinx.serialization para JSON, voce tem um stack moderno e performático para o backend.

Se está começando com Kotlin no servidor, o Ktor é uma excelente porta de entrada — mais acessível que o Spring Boot e com uma experiência de desenvolvimento verdadeiramente Kotlin-first. Se quiser comparar com frameworks de outras linguagens, veja como Go lida com APIs usando Gin e Echo ou como Python aborda o problema com FastAPI e Django.