Neste tutorial, vamos construir uma API REST completa usando o Ktor, o framework web assíncrono e leve criado pela JetBrains especificamente para Kotlin. Você vai aprender desde a configuração inicial do projeto até a integração com banco de dados usando Exposed, passando por routing, serialização JSON, autenticação e testes. Se você já tem familiaridade com funções e coroutines em Kotlin, está pronto para começar.
O que é o Ktor?
Ktor é um framework assíncrono para criar aplicações web e microsserviços em Kotlin. Diferente de frameworks como Spring Boot, o Ktor é minimalista por design — você adiciona apenas os recursos (chamados de plugins) que precisa. Ele é construído sobre coroutines, o que significa que cada requisição é tratada de forma não-bloqueante usando suspend functions.
A arquitetura do Ktor é baseada em um pipeline de plugins que interceptam e processam requisições HTTP. Isso torna o framework extremamente flexível e performático, ideal para microsserviços e APIs de alta concorrência.
Passo 1: Configuração do Projeto
A forma mais rápida de iniciar um projeto Ktor é usando o gerador online em start.ktor.io ou configurando manualmente o build.gradle.kts. Vamos pelo caminho manual para entender cada dependência.
// build.gradle.kts
plugins {
kotlin("jvm") version "2.0.0"
id("io.ktor.plugin") version "2.3.12"
kotlin("plugin.serialization") version "2.0.0"
}
group = "com.exemplo"
version = "1.0.0"
application {
mainClass.set("com.exemplo.ApplicationKt")
}
dependencies {
// Ktor Server
implementation("io.ktor:ktor-server-core-jvm")
implementation("io.ktor:ktor-server-netty-jvm")
implementation("io.ktor:ktor-server-content-negotiation-jvm")
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
implementation("io.ktor:ktor-server-auth-jvm")
implementation("io.ktor:ktor-server-auth-jwt-jvm")
// Exposed (ORM)
implementation("org.jetbrains.exposed:exposed-core:0.52.0")
implementation("org.jetbrains.exposed:exposed-dao:0.52.0")
implementation("org.jetbrains.exposed:exposed-jdbc:0.52.0")
implementation("com.h2database:h2:2.2.224")
// Testes
testImplementation("io.ktor:ktor-server-tests-jvm")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:2.0.0")
}
Observe que usamos o servidor Netty como engine. O Ktor suporta múltiplas engines (CIO, Jetty, Tomcat), mas Netty é a escolha mais comum para produção.
Passo 2: Criando o Servidor e Definindo Rotas
O ponto de entrada da aplicação Ktor é a função embeddedServer. Dentro dela, configuramos plugins e definimos rotas usando a DSL de routing.
// src/main/kotlin/com/exemplo/Application.kt
package com.exemplo
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.routing.*
import io.ktor.server.response.*
import io.ktor.http.*
fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
configurarRotas()
}.start(wait = true)
}
fun Application.configurarRotas() {
routing {
get("/") {
call.respondText("Bem-vindo à API Kotlin Brasil!", ContentType.Text.Plain)
}
route("/api/v1") {
get("/status") {
call.respondText("OK", ContentType.Text.Plain)
}
}
}
}
A DSL de routing do Ktor utiliza lambdas com receiver, permitindo definir rotas de forma declarativa e organizada. Cada bloco get, post, put e delete recebe o caminho da rota e uma suspend function que trata a requisição.
Passo 3: Serialização JSON com kotlinx.serialization
Para trabalhar com JSON, precisamos instalar o plugin ContentNegotiation e configurar o serializador.
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.Serializable
@Serializable
data class Tarefa(
val id: Int,
val titulo: String,
val concluida: Boolean = false
)
fun Application.configurarSerializacao() {
install(ContentNegotiation) {
json()
}
}
Agora podemos criar endpoints que recebem e retornam objetos Kotlin automaticamente serializados como JSON. Veja como fica um CRUD básico de tarefas:
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.http.*
val tarefas = mutableListOf<Tarefa>()
fun Application.rotasTarefas() {
routing {
route("/api/tarefas") {
get {
call.respond(tarefas)
}
get("/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
val tarefa = tarefas.find { it.id == id }
if (tarefa != null) {
call.respond(tarefa)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("erro" to "Tarefa não encontrada"))
}
}
post {
val tarefa = call.receive<Tarefa>()
tarefas.add(tarefa)
call.respond(HttpStatusCode.Created, tarefa)
}
delete("/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
val removida = tarefas.removeIf { it.id == id }
if (removida) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound)
}
}
}
}
}
Passo 4: Autenticação com JWT
O Ktor fornece plugins de autenticação prontos para uso. Vamos configurar autenticação via JWT (JSON Web Token), uma das abordagens mais comuns em APIs REST.
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
fun Application.configurarAutenticacao() {
val secret = "meu-segredo-super-secreto"
val issuer = "kotlin-brasil"
val audience = "api-usuarios"
install(Authentication) {
jwt("auth-jwt") {
realm = "Acesso à API"
verifier(
JWT.require(Algorithm.HMAC256(secret))
.withAudience(audience)
.withIssuer(issuer)
.build()
)
validate { credential ->
if (credential.payload.audience.contains(audience)) {
JWTPrincipal(credential.payload)
} else null
}
}
}
routing {
authenticate("auth-jwt") {
get("/api/perfil") {
val principal = call.principal<JWTPrincipal>()
val email = principal!!.payload.getClaim("email").asString()
call.respond(mapOf("email" to email))
}
}
}
}
Rotas protegidas ficam dentro do bloco authenticate, garantindo que apenas requisições com token válido serão processadas.
Passo 5: Integração com Banco de Dados usando Exposed
O Exposed é o ORM oficial da JetBrains para Kotlin. Ele oferece duas abordagens: DSL (estilo SQL) e DAO (estilo Active Record). Vamos usar a abordagem DSL.
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
object Tarefas : Table("tarefas") {
val id = integer("id").autoIncrement()
val titulo = varchar("titulo", 255)
val concluida = bool("concluida").default(false)
override val primaryKey = PrimaryKey(id)
}
fun inicializarBanco() {
Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;", driver = "org.h2.Driver")
transaction {
SchemaUtils.create(Tarefas)
}
}
fun listarTarefas(): List<Tarefa> = transaction {
Tarefas.selectAll().map {
Tarefa(
id = it[Tarefas.id],
titulo = it[Tarefas.titulo],
concluida = it[Tarefas.concluida]
)
}
}
fun inserirTarefa(tarefa: Tarefa): Int = transaction {
Tarefas.insert {
it[titulo] = tarefa.titulo
it[concluida] = tarefa.concluida
}[Tarefas.id]
}
Para integrar com o Ktor, chame inicializarBanco() no módulo da aplicação e substitua a lista em memória pelas funções do Exposed nas rotas.
Passo 6: Testando a Aplicação Ktor
O Ktor inclui um módulo de testes que permite simular requisições HTTP sem iniciar um servidor real, usando testApplication.
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.testing.*
import kotlin.test.*
class TarefasTest {
@Test
fun `deve retornar status OK`() = testApplication {
application {
configurarSerializacao()
configurarRotas()
}
client.get("/api/v1/status").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals("OK", bodyAsText())
}
}
@Test
fun `deve criar tarefa via POST`() = testApplication {
application {
configurarSerializacao()
rotasTarefas()
}
client.post("/api/tarefas") {
contentType(ContentType.Application.Json)
setBody("""{"id": 1, "titulo": "Estudar Ktor", "concluida": false}""")
}.apply {
assertEquals(HttpStatusCode.Created, status)
}
}
}
O testApplication cria um ambiente isolado onde você pode testar cada módulo da sua aplicação independentemente, sem necessidade de subir o servidor Netty.
Deploy Básico
Para gerar um JAR executável, configure o plugin ktor no Gradle e execute:
// build.gradle.kts
ktor {
fatJar {
archiveFileName.set("app.jar")
}
}
Depois, basta rodar ./gradlew buildFatJar e executar com java -jar build/libs/app.jar. Para containerizar, crie um Dockerfile simples baseado em uma imagem JVM como eclipse-temurin:21-jre-alpine.
Erros Comuns
1. Esquecer de instalar o plugin ContentNegotiation: Sem ele, o Ktor não sabe serializar objetos para JSON e retorna erro 500. Sempre chame install(ContentNegotiation) { json() } antes de usar call.respond com data classes.
2. Não tratar parâmetros nulos nas rotas: call.parameters["id"] retorna nullable String?. Sempre use toIntOrNull() e trate o caso nulo para evitar exceções em runtime.
3. Bloquear a thread principal com operações de banco: O Exposed usa JDBC, que é bloqueante. Em produção, envolva as chamadas em newSuspendedTransaction do módulo exposed-kotlin ou use withContext(Dispatchers.IO) para não bloquear as coroutines do Ktor.
4. Não configurar CORS: Se sua API será consumida por um frontend em outro domínio, instale o plugin CORS do Ktor. Sem ele, o navegador bloqueará as requisições.
5. JWT secret hardcoded no código: Nunca deixe secrets diretamente no código fonte. Use variáveis de ambiente com System.getenv("JWT_SECRET") ou arquivos de configuração do Ktor (application.conf).
Conclusão e Próximos Passos
Neste tutorial, construímos uma API REST funcional com Ktor cobrindo os pilares fundamentais: routing, serialização, autenticação JWT, integração com banco de dados Exposed e testes automatizados. O Ktor é uma excelente escolha para quem quer construir backends em Kotlin puro, aproveitando ao máximo as coroutines e a expressividade da linguagem.
Como próximos passos, recomendamos explorar o Ktor Client para fazer requisições HTTP a outros serviços, configurar WebSockets para comunicação em tempo real, e integrar com bancos de dados de produção como PostgreSQL. Você também pode estudar o uso de Koin ou Kodein para injeção de dependências, tornando sua aplicação mais modular e testável.
Para aprofundar seus conhecimentos em Kotlin para backend, confira nosso tutorial sobre Coroutines Avançadas e o guia Kotlin para Backend.