Utilizando o Ktor para criar um CRUD e REST API com Kotlin
16 min16 minutos de leitura
16 min16 minutos de leitura
Autor(a)
Alex Felipe
Alex é instrutor e desenvolvedor e possui experiência em Java, Kotlin, Android. Atualmente cria conteúdo no canal https://www.youtube.com/@AlexFelipeDev.
Inscreva-se em nossa Newsletter
Fique por dentro de conteúdos, insights e oportunidades do universo tech. Receba novidades e lançamentos direto no seu e-mail.
Existem diversas ferramentas capazes de criar uma aplicação Web, como, por exemplo, Node.js, Django, Spring Framework, Ruby on Rails etc.
Embora exista muitas opções, para você que aprendeu Kotlin, é natural utilizar ferramentas que deem suporte para linguagens interoperáveis, como, por exemplo, o Spring Framework para Java.
Por mais que seja uma opção válida, existem outras opções para criar aplicações Web com o Kotlin, dentre elas, temos o Ktor, um framework desenvolvido pela Jetbrains totalmente em Kotlin e baseado em Coroutines. O principal objeto do Ktor é criar aplicações assíncronas, seja cliente ou servidora, de uma maneira fácil e idiomática ao Kotlin.
E agora que tivemos uma introdução do que é o Ktor, neste artigo, eu vou te mostrar como criar uma aplicação servidora e implementar um CRUD, bora? 😎
Neste artigo, vamos considerar o uso do gerador de projeto Ktor, pois é uma ferramenta gratuita que permite utilizar uma IDE de nossa preferência que dê suporte ao Ktor, nesse caso, vou considerar o IntelliJ IDEA Community.
Você pode baixar qualquer IDE da Jetbrains pelo Toolbox. Recomendo que utilize um instalador de ferramentas da Jetbrains com o objetivo de facilitar o download e configuração. 😉
Para nosso exemplo, vou criar o projeto Ceep, um App de notas com título e descrição:
No momento que este artigo foi escrito, o Ktor estava na versão 2.2.4, ou seja, consequentemente, vão surgir novas versões e pode ser que o processo de criação de projeto tenha etapas diferentes, seja pelo visual do site ou outros plugins necessários etc.
Agora, vamos entender o que aconteceu no gif:
O nome do projeto foi Ceep;
Ao clicar em Adjusting project settings, acessamos as opções específicas do projeto:
Build system → definição de build tool, por padrão temos o Gradle Kotlin;
Website → (alura.com.br) vai determinar o pacote e artefato (br.com.alura.ceep) utilizado;
Ktor version → versão do Ktor (2.2.4);
Engine → definição de quem será responsável por gerenciar a conexão entre o servidor e cliente. Nessa amostra, o Netty é o padrão, mas você pode consultar outras opções de Engines suportadas e escolher a de sua preferência;
Configuration in → definição da escrita de configuração, sendo a Code em código Kotlin, e as demais em YAML ou HOCON. Aqui, vou manter a opção em código Kotlin;
Add sample code → essa opção é interessante para o projeto ter uma amostra de configuração inicial.
Depois dos ajustes do projeto, clicamos em Add plugins para adicionar ferramentas do Ktor:
Routing → permite fazer a configuração de rotas para acessar a aplicação servidora;
Exposed → adiciona a lib Exposed que é uma abstração em Kotlin para comunicar com o banco de dados. Um detalhe importante é que ao adicionar o Exposed, mais 2 plugins foram adicionados:
Content Negotiation → adiciona conversão automática de acordo com Content-Type, como por exemplo, JSON ou XML;
kotlinx.serialization → conversor de JSON para objetos feito em Kotlin.
Depois de adicionar todas essas configurações, é só clicar em Generate project, baixar o zip e extrair em algum local onde você mantém seus projetos.
Abrindo o projeto Ktor no IntelliJ IDEA Community
Com acesso ao projeto, abrimos o mesmo a partir da opção “Open” do IntelliJ IDEA:
A partir desse momento, o IntelliJ vai realizar algumas tarefas para baixar as dependências, indexar arquivos etc, e então temos o seguinte resultado ou similar:
Basicamente, o Application.kt é o ponto de partida da aplicação com o Ktor, basta executar a função main() e aguardar o log apresentar uma mensagem similar a esta:
[main]INFOktor.application-Autoreloadisdisabledbecausethedevelopmentmodeisoff.
[main]DEBUGExposed-SELECTSETTING_VALUEFROMINFORMATION_SCHEMA.SETTINGSWHERESETTING_NAME = 'MODE'
[main]DEBUGExposed-CREATETABLEIFNOTEXISTSUSERS (ID INT AUTO_INCREMENT PRIMARY KEY, "NAME"VARCHAR(50) NOT NULL, AGE INT NOT NULL)
[main]INFOktor.application-Applicationstartedin1.056seconds.
[DefaultDispatcher-worker-1]INFOktor.application-Respondingathttp://127.0.0.1:8080
Embora tenha algumas informações de inicialização, como autoreload ou criação de tabela no banco, para esse momento, é importante notar a mensagem de inicialização e que a aplicação está respondendo no endereço [http://127.0.0.1:8080](http://127.0.0.1:8080), ou seja, é só acessar esse endereço pelo navegador, ou então, [http://localhost:8080](http://localhost:8080) se preferir:
A aplicação retorna Hello World! por padrão! Embora o código seja simples, não é claro em qual ponto do código essa configuração foi feita, concorda? Sendo assim, a seguir, vamos analisar o código pronto e entender o que está acontecendo.
Conhecendo os códigos do Ktor
Para entender o processo que foi realizado anteriormanete, preciso fazer uma análise mais detalhada do código, começando pelo código do Application.kt:
main() → ponto de partida da aplicação, assim como qualquer outra aplicação Kotlin;
embbededServer() → cria um servidor embutido com base em um factory, nesse caso, o da Engine Netty;
port → indica a porta de execução;
host → configura o endereço de execução da aplicação, sendo "0.0.0.0" o famoso [localhost](http://localhost) ou 127.0.0.1;
module → determina onde os módulos do Ktor estão configurados, nesse caso, na função de extensão Application.module();
start(wait = true) → inicia a aplicação Ktor e a mantém em execução até que seja encerrada, por isso é necessário enviar o true para wait.
Application.module() → função para adicionar e configurar todos os plugins do projeto:
configureSerialization() → serialização de objetos;
configureDatabases() → banco de dados;
configureRouting() → mapeamento de rotas.
Agora que sabemos o que cada código faz, vamos explorar a implementação das configurações contidas na Application.module(), pois são os códigos que nós iremos personalizar com base na nossa regra de negócio.
Configuração de serialização
Na configuração de serialização, temos o seguinte código:
fun Application.configureSerialization() {
install(ContentNegotiation) {
json()
}
routing {
get("/json/kotlinx-serialization") {
call.respond(mapOf("hello" to "world"))
}
}
}
Perceba que se trata de uma outra extensão de Application, isso acontece pois a Application é a referência central do Ktor, ou seja, todas as configurações ou requisições são de responsabilidade dela. Seguindo com o código, podemos compreender o seguinte:
install(ContentNegotiation) → instala plugins ao Ktor, nesse caso o plugin ContentNegotiation e permite realizar a configuração via lambda;
json() → função de configuração do plugin ContentNegotiation que registra a aceitação de JSON durante a comunicação HTTP, o famoso Content-Type.
Instalações de plugins geralmente vão oferecer uma lambda para realizar as configurações.
routing → instala o plugin de mapeamento;
get("/json/kotlinx-serialization") → mapeia uma requisição para GET com o endereço [http://localhost:8080/json/kotlinx-serialization](http://localhost:8080/json/kotlinx-serialization);
call.respond(mapOf("hello" to "world")) → configura a resposta para a chamada GET e retorna um JSON:
Veja que apenas com essa configuração temos uma requisição GET que devolve um JSON.
Configuração de mapeamento de rotas
Embora a configuração de banco de dados seja a segunda etapa, ela é a mais complexa. Por isso, a próxima que iremos tratar será a de mapeamento de rotas:
Observe que o código não tem tanto segredo, considerando a parte de serialização. É necessário instalar o plugin de roteamento e definir o end-point inicial com o texto "Hello World!". É por meio dessa configuração que vimos o texto ao acessar o App na primeira execução!
Configuração de banco de dados
Agora, vamos para a configuração mais complexa e que apresenta a maior parte de detalhes de como iremos implementar a nossa aplicação:
fun Application.configureDatabases() {
val database = Database.connect(
url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
user = "root",
driver = "org.h2.Driver",
password = ""
)
val userService = UserService(database)
routing {
// Create userpost("/users") {
val user = call.receive<User>()
val id = userService.create(user)
call.respond(HttpStatusCode.Created, id)
}
// Read userget("/users/{id}") {
val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID")
val user = userService.read(id)
if (user != null) {
call.respond(HttpStatusCode.OK, user)
} else {
call.respond(HttpStatusCode.NotFound)
}
}
// Update userput("/users/{id}") {
val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID")
val user = call.receive<User>()
userService.update(id, user)
call.respond(HttpStatusCode.OK)
}
// Delete userdelete("/users/{id}") {
val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID")
userService.delete(id)
call.respond(HttpStatusCode.OK)
}
}
}
Agora já temos bem mais código, não é mesmo? Então, bora entender as novidades:
Database.connect() → abre a conexão com um banco de dados a partir do endereço, usuário, senha e driver de conexão;
A amostra utiliza o h2 como banco de dados, mas poderia ser outros bancos que possuem drivers suportados pelo jdbc.
UserService → código de abstração para a comunicação com o banco de dados.
Esse código não tem relação com o Ktor, ou seja, é uma camada de abstração totalmente personalizável para a nossa regra de negócio e por isso o analisaremos por último.
Novamente, temos o plugin de mapeamento de rotas, mas a diferença é que já temos uma representação de CRUD, ou seja, uma requisição para ações de:
inserção (post);
busca (get);
alteração (put);
remoção (delete).
Note que os código são similares, a grande diferença está em:
Rotas diferentes, algumas fixas ou com variações que podem receber parâmetros, como é o caso do id;
Chamadas ao service para realizar a operação no banco de dados;
Retornos que podem ser fixos ou condicionais, como por exemplo, na busca de usuário pode não existir o usuário esperado e é retornado um HttpStatusCode.NotFound.
Pronto! Conhecemos os códigos do Ktor e sabemos o que fazem, mas ainda precisamos analisar o código do service para compreender o que iremos personalizar para implementar o nosso CRUD. Vamos lá?
Analisando o código do service
No UserService, a seguinte implementação inicial é feita:
Repare que além do service, foi implementado o objeto que representa o modelo, requisição e resposta, o User.
A anotação @Serializable indica que esse objeto pode realizar a conversão de JSON para objeto e vice-versa a partir da lib do Kotlin de serialização.
E então, temos o restante do código:
object Users : Table() → representa a tabela no exposed;
transaction(database) → abre uma transação no banco de dados e permite realizar operações via lambda;
SchemaUtils.create(Users) → cria a tabela baseada no objeto do tipo Table do exposed, que nesse caso é a tabela de usuários;
dbQuery() → encapsula o código de criação de transação via coroutines com o escopo de IO.
Os demais métodos, basicamente, fazem as ações de CRUD esperada, ou seja, vai abrir uma transação com coroutines, enviar os valores e obter um retorno esperado. Pronto! Fizemos a análise necessária do código de amostra e podemos começar o nosso!
Criando os modelos de nota
Vamos começar com o modelo para a nossa nota:
//Note.ktpackage br.com.alura.models
import br.com.alura.responses.NoteResponse
import java.util.*
classNote(
val id: UUID = UUID.randomUUID(),
val title: String,
val message: String
)
fun Note.toNoteResponse(): NoteResponse {
return NoteResponse(
id = id.toString(),
title = title,
message = message
)
}
Diferente da amostra inicial, vamos utilizar UUID para identificar os modelos, mantê-lo num pacote específico (models) e o modelo será diferente dos objetos de requisição e resposta.
Também aproveitei para implementar o conversor de resposta a partir de uma extensão do modelo. Agora, vamos para a requisição e resposta:
//NoteRequest.ktpackage br.com.alura.requests
import br.com.alura.models.Note
import kotlinx.serialization.Serializable
import java.util.UUID
@SerializableclassNoteRequest(
val title: String,
val message: String
)
fun NoteRequest.toNote(
id: UUID = UUID.randomUUID()
): Note {
return Note(
id = id,
title = title,
message = message
)
}
Também adicionei uma função de extensão de requisição para o modelo de nota. E a resposta ficou assim:
//NoteResponse.ktpackage br.com.alura.responses
import kotlinx.serialization.Serializable
@SerializableclassNoteResponse(
val id: String,
val title: String,
val message: String
)
Embora o id do modelo de nota seja UUID, no modelo de resposta utilizei String. O motivo dessa decisão é para facilitar a implementação, pois para tipos não primitivos é necessário realizar configurações extras com o serializador do Kotlin.
No caso da resposta, não há a necessidade de um conversor. Se você preferir, também pode utilizar data class nas implementações de modelos.
Embora o código seja relativamente grande, ele realiza os mesmos comportamentos do UserService, com a adição do método findAll() que faz a busca de todas as notas existentes no banco de dados.
Além disso, ao invés de ter um método para criar e outro para alterar, criei apenas o save(), que cria uma nota nova caso ela não exista no banco ou a altera se ela existir, baseada na chave primária (o id).
Por fim, implementei a função de extensão ResultRow.toNote() para reutilizar a conversão de uma linha para o modelo de nota.
Mapeamento os end-points para as notas
Após a implementação, podemos começar com o mapeamentos dos end-points das notas. E para isso, vamos criar o nosso módulo de routing:
//NoteRouting.kt
package br.com.alura.modules
import br.com.alura.models.toNoteResponse
import br.com.alura.requests.NoteRequest
import br.com.alura.requests.toNote
import br.com.alura.services.NoteService
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import java.util.*
fun Application.configureNoteRouting(
service: NoteService
) {
routing {
get("/notes") {
val response = service.findAll().map {
it.toNoteResponse()
}
call.respond(HttpStatusCode.OK, response)
}
get("/notes/{id}") {
val id = UUID.fromString(call.parameters["id"])
service.findById(id)?.let { note ->
val response = note.toNoteResponse()
call.respond(HttpStatusCode.OK, response)
} ?: call.respond(HttpStatusCode.NotFound)
}
post("/notes") {
val note = call.receive<NoteRequest>().toNote()
val response = service.save(note).toNoteResponse()
call.respond(HttpStatusCode.Created, response)
}
put("/notes/{id}") {
val id = UUID.fromString(call.parameters["id"])
val note = call.receive<NoteRequest>().toNote(id)
val response = service.save(note).toNoteResponse()
call.respond(HttpStatusCode.OK, response)
}
delete("/notes/{id}") {
val id = UUID.fromString(call.parameters["id"])
service.delete(id)
call.respond(HttpStatusCode.OK)
}
}
}
Pronto, agora falta apenas integrar o nosso código com a Application do Kotlin.
Configurando o Ktor com a regra de negócio
Na função Application.module(), fazemos a seguintes modificações:
Não utilizamos mais nenhuma função de configuração que veio na amostra inicial;
As instalações de plugins, configuração de banco de dados e criação do service são feitas antes de chamar o método de configuração de mapeamento;
Agora, o banco de dados é configurado para criar um arquivo no diretório database (file:./database/db) no local onde o projeto é executado, dessa forma, os dados são mantidos mesmo que a aplicação seja reiniciada.
Ao rodar a aplicação, temos o seguinte resultado via log:
[main] INFO ktor.application - Autoreload is disabled because the development mode is off.
[main] DEBUG Exposed - SELECT SETTING_VALUE FROM INFORMATION_SCHEMA.SETTINGS WHERE SETTING_NAME = 'MODE'
[main] DEBUG Exposed - CREATE TABLE IF NOT EXISTS NOTES (ID UUID PRIMARY KEY, TITLE VARCHAR(255) NOT NULL, MESSAGE TEXT NOT NULL)
[main] INFO ktor.application - Application started in 0.808 seconds.
[DefaultDispatcher-worker-1] INFO ktor.application - Responding at http://127.0.0.1:8080
Veja que a aplicação ainda roda sem apresentar problemas e, ao invés de criar a tabela de usuário, é criada a de notas! E agora que a nossa API está funcionando, podemos fazer os testes com um cliente HTTP, como por exemplo, o Postman.
Pronto! Realizando todos esses testes, teremos o O CRUD de notas em Ktor funcionando corretamente!
O Ktor é uma ferramenta capaz de implementar uma aplicação servidora, porém, também é uma ferramenta que atua no lado do cliente realizando requisições HTTP. Se você tem interesse em como fazer isso em uma aplicação Android, confira o artigo Consumindo REST API no Android com o Ktor, aqui da Alura.
Conclusão
Neste artigo aprendemos o básico para criar uma REST API simples com o Ktor, vimos como é possível criar um projeto com códigos de amostra para mapeamento de rotas, banco de dados e serialização. Também vimos como podemos personalizar o código para implementar um CRUD de notas e fizemos o teste da implementação a partir do Postman.
Aproveite esse momento para praticar e implementar a sua própria API. Compartilhe suas impressões com a gente nas redes sociais ou Discord. Dessa forma, entendemos o quão relevante é o conteúdo e aumentam as chances de produzirmos mais conteúdos sobre o Ktor. 😉