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.IOpara operacoes bloqueantes. - Use configuracao externalizada: o arquivo
application.confpermite configuracoes diferentes por ambiente.
Erros Comuns e Armadilhas
- Esquecer o plugin de serializacao: sem
ContentNegotiationconfigurado, o Ktor nao serializa/deserializa objetos automaticamente. Voce recebera erros ao usarcall.receiveoucall.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.