Utilizando o Ktor para criar um CRUD e REST API com Kotlin
![Utilizando o Ktor para criar um CRUD e REST API com Kotlin](assets/utilizando-ktor-criar-crud-rest-api-kotlin/utilizando-ktor-criar-crud-rest-api-kotlin.png)
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.
Inclusive, já desenvolvi algumas REST APIs aqui na Alura com o Spring Boot utilizando o Kotlin e se você já fez os cursos de Android, muito provavelmente já usou alguma delas. 👀
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? 😎
Criando o projeto com o Ktor
Para criar um projeto com o Ktor, podemos considerar as seguintes opções:
- IntelliJ IDEA Ultimate (versão paga);
- Gerador de projeto Ktor.
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:
![Página de gerador de projeto Ktor, inicialmente, na seção setting, é adicionado o nome do projeto e configurações de estrutura, ao clicar em add plugins são adicionados os plugins necessários. Por fim, ao clicar Generate project, o produto com as configurações realizadas é baixado como um zip.](assets/utilizando-ktor-criar-crud-rest-api-kotlin/imagem1.gif)
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.
- Build system → definição de build tool, por padrão temos o
- 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:
![Tela de plugins do gerador de projeto Ktor, apresentando 4 plugins adicionados, sendo o Content Negotiation e o kotlinx.serialization os 2 novos que vieram junto com o exposed.](assets/utilizando-ktor-criar-crud-rest-api-kotlin/imagem2.jpg)
- 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.
![Banner da Escola de Programação: Matricula-se na escola de Programação. Junte-se a uma comunidade de mais de 500 mil estudantes. Na Alura você tem acesso a todos os cursos em uma única assinatura; tem novos lançamentos a cada semana; desafios práticos. Clique e saiba mais!](assets/alura-matricula-maior-escola-tecnologia-brasil-mais-500-mil-estudantes/matricula-escola-programacao-alura-saiba-mais-versao-mobile.png)
Abrindo o projeto Ktor no IntelliJ IDEA Community
Com acesso ao projeto, abrimos o mesmo a partir da opção “Open” do IntelliJ IDEA:
![Tela de launcher do IntelliJ IDEA Community. Ao clicar em Open, apresenta o explorar de arquivos e busca o projeto baixado já extraído do arquivo zip. Ao encontrá-lo, selecioná-lo e clicar em OK; a IDE começa o processo para abrir o projeto.](assets/utilizando-ktor-criar-crud-rest-api-kotlin/imagem3.gif)
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:
![Tela do IntelliJ exibindo a aba de projeto com os arquivos contidos no projeto.](assets/utilizando-ktor-criar-crud-rest-api-kotlin/imagem4.jpg)
Nesta amostra estou usando a nova interface de usuário do IntelliJ IDEA.
Agora, podemos acessar o arquivo src\main\kotlin\br\com\alura\Application.kt
e ter acesso ao código inicial da aplicação:
package br.com.alura
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import br.com.alura.plugins.*
fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module)
.start(wait = true)
}
fun Application.module() {
configureSerialization()
configureDatabases()
configureRouting()
}
Rodando a aplicação Ktor
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] 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 USERS (ID INT AUTO_INCREMENT PRIMARY KEY, "NAME" VARCHAR(50) NOT NULL, AGE INT NOT NULL)
[main] INFO ktor.application - Application started in 1.056 seconds.
[DefaultDispatcher-worker-1] INFO ktor.application - Responding at http://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:
![Tela do navegador acessando o endereço http://localhost:8080 e apresentando a mensagem Hello World!](assets/utilizando-ktor-criar-crud-rest-api-kotlin/imagem5.jpg)
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)
ou127.0.0.1
;module
→ determina onde os módulos do Ktor estão configurados, nesse caso, na função de extensãoApplication.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 otrue
parawait
.
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 pluginContentNegotiation
e permite realizar a configuração via lambda;json()
→ função de configuração do pluginContentNegotiation
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:
fun Application.configureRouting() {
routing {
get("/") {
call.respondText("Hello World!")
}
}
}
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 user
post("/users") {
val user = call.receive<User>()
val id = userService.create(user)
call.respond(HttpStatusCode.Created, id)
}
// Read user
get("/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 user
put("/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 user
delete("/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:
@Serializable
data class User(val name: String, val age: Int)
class UserService(private val database: Database) {
object Users : Table() {
val id = integer("id").autoIncrement()
val name = varchar("name", length = 50)
val age = integer("age")
override val primaryKey = PrimaryKey(id)
}
init {
transaction(database) {
SchemaUtils.create(Users)
}
}
suspend fun <T> dbQuery(block: suspend () -> T): T =
newSuspendedTransaction(Dispatchers.IO) { block() }
suspend fun create(user: User): Int = dbQuery {
Users.insert {
it[name] = user.name
it[age] = user.age
}[Users.id]
}
suspend fun read(id: Int): User? {
return dbQuery {
Users.select { Users.id eq id }
.map { User(it[Users.name], it[Users.age]) }
.singleOrNull()
}
}
suspend fun update(id: Int, user: User) {
dbQuery {
Users.update({ Users.id eq id }) {
it[name] = user.name
it[age] = user.age
}
}
}
suspend fun delete(id: Int) {
dbQuery {
Users.deleteWhere { Users.id.eq(id) }
}
}
}
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 tipoTable
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.kt
package br.com.alura.models
import br.com.alura.responses.NoteResponse
import java.util.*
class Note(
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.kt
package br.com.alura.requests
import br.com.alura.models.Note
import kotlinx.serialization.Serializable
import java.util.UUID
@Serializable
class NoteRequest(
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.kt
package br.com.alura.responses
import kotlinx.serialization.Serializable
@Serializable
class NoteResponse(
val id: String,
val title: String,
val message: String
)
Embora o
id
do modelo de nota sejaUUID
, no modelo de resposta utilizeiString
. 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.
Implementando o service de notas
Agora vamos implementar o NoteService
:
//NoteService.kt
package br.com.alura.services
import br.com.alura.models.Note
import kotlinx.coroutines.Dispatchers
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.jetbrains.exposed.sql.transactions.transaction
import java.util.*
class NoteService(database: Database) {
private object Notes : Table() {
val id = uuid("id")
val title = varchar("title", 255)
val message = text("message")
override val primaryKey = PrimaryKey(id)
}
init {
transaction(database) {
SchemaUtils.create(Notes)
}
}
private suspend fun <T> dbQuery(block: suspend () -> T): T =
newSuspendedTransaction(Dispatchers.IO) { block() }
suspend fun findAll(): List<Note> = dbQuery {
Notes.selectAll()
.map { row -> row.toNote() }
}
suspend fun findById(id: UUID): Note? {
return dbQuery {
Notes.select { Notes.id eq id }
.map { row -> row.toNote() }
.singleOrNull()
}
}
suspend fun save(note: Note): Note = dbQuery {
Notes.insertIgnore {
it[id] = note.id
it[title] = note.title
it[message] = note.message
}.let {
Note(
id = it[Notes.id],
title = it[Notes.title],
message = it[Notes.message]
)
}
}
suspend fun delete(id: UUID) {
dbQuery {
Notes.deleteWhere { Notes.id.eq(id) }
}
}
private fun ResultRow.toNote() = Note(
id = this[Notes.id],
title = this[Notes.title],
message = this[Notes.message]
)
}
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:
package br.com.alura
import br.com.alura.modules.configureNoteRouting
import br.com.alura.services.NoteService
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.contentnegotiation.*
import org.jetbrains.exposed.sql.Database
fun main() {
embeddedServer(
Netty,
port = 8080,
host = "0.0.0.0",
module = Application::module
).start(wait = true)
}
fun Application.module() {
install(ContentNegotiation) {
json()
}
val database = Database.connect(
url = "jdbc:h2:file:./database/db",
user = "root",
driver = "org.h2.Driver",
password = ""
)
val service = NoteService(database)
configureNoteRouting(service)
}
Veja que temos algumas diferenças:
- 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.
- Agora, o banco de dados é configurado para criar um arquivo no diretório database (
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.
Testando a API com o Postman
Caso deseje, você pode baixar a coleção do Postman para fazer os testes. E para realizar o testes, siga a sequência:
- Crie uma nota:
![Realizando a requisição POST no endereço http://localhost:8080/notes. É enviada uma nota via corpo da requisição e retorna a nota criada com o código 201.](assets/utilizando-ktor-criar-crud-rest-api-kotlin/imagem6.jpg)
- Busque todas as notas:
![Realiza a requisição GET no endereço http://localhost:8080/notes. Não são enviados dados via corpo da requisição e retorna uma lista de notas com o código 200.](assets/utilizando-ktor-criar-crud-rest-api-kotlin/imagem7.jpg)
- Busque uma nota específica:
![Realiza a requisição GET no endereço http://localhost:8080/notes/dba5834f-de4b-4b54-91e5-cc1a2b162188. Não são enviados dados via corpo da requisição e retorna uma nota e código 200.](assets/utilizando-ktor-criar-crud-rest-api-kotlin/imagem8.jpg)
- Altere uma nota existe ou salvando:
![Realiza requisição PUT no endereço http://localhost:8080/notes/dba5834f-de4b-4b54-91e5-cc1a2b162188. É enviada uma nota com informações que deseja alterar via corpo da requisição e retorna a nota alterada com o código 200.](assets/utilizando-ktor-criar-crud-rest-api-kotlin/imagem9.jpg)
- Remova nota:
![Realiza a requisição DELETE no endereço http://localhost:8080/notes/dba5834f-de4b-4b54-91e5-cc1a2b162188. Não são enviados dados via corpo da requisição e retorna apenas o código 200.](assets/utilizando-ktor-criar-crud-rest-api-kotlin/imagem10.jpg)
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.
Se você tem interesse no projeto, pode consultar o repositório no GitHub para mais detalhes, ou então, o código fonte.
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. 😉
Bons estudos e até mais!