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.