Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
July 18, 2021 09:21 pm GMT

Criando uma API com Ktor

Kotlinautas

Esse contedo oferecido e distribudo pela comunidade Kotlinautas, uma comunidade brasileira que busca oferecer contedo gratuito sobre a linguagem Kotlin em um espao plural.

capa Kotlinautas

Introduo

O qu Ktor?

Ktor um framework para Kotlin para criar microservios, websockets, MVC's e servios web em geral.

Ktor tem uma diferena em relao aos frameworks que geralmente so usados com a linguagem Kotlin. Ktor feito inteiramente em Kotlin, diferente de outros frameworks, como Spring ou Micronaut. Essa diferena faz com que o Ktor se encaixe perfeitamente com Kotlin, enquanto que os outros frameworks que no so feitos inteiramente em Kotlin (Pois foram feitos inicialmente para Java) no conseguem fazer.

Ktor tem um site oficial onde pode ser encontrada a sua documentao.

O qu iremos construir?

Vamos construir uma API simples, que possibilite a criao de usurios e de artigos, e cada artigo ser pertencente um nico usurio. A funo deste exerccio apenas aprender como uma aplicao funciona no Ktor, como podemos criar rotas, como funciona um model, como conectar um banco de dados, etc.

Materiais

Para este artigo, recomendo que voc utilize a IDE IntelliJ. tanto a verso gratuita Community, quanto a paga Ultimate podero ser usadas neste artigo. Pois com ela, processos como instalar as dependncias do Gradle, executar a aplicao, escrever o cdigo, etc. se tornaro bem mais fceis.

Tambm necessrio ter o Kotlin instalado na mquina, e um conhecimento mnimo da linguagem.

Criando um projeto

O site oficial oferece um mtodo para criarmos um projeto pronto do Ktor, isso significa que podemos escolher diversas features, bibliotecas, configuraes, etc. para j virem no projeto. Isso facilita a criao de um projeto, pois podemos apenas baixar o arquivo ZIP deste projeto pronto, e descomprimi-lo para comearmos a codar.

Site start.ktor.io

Dentro do site start.ktor.io, deixe as opes do menu assim, com o website sendo konteudo, o artifact sendo konteudo.konteudo, e a engine como Netty.

Configuraes recomendadas para o projeto

Na parte de plugins, adicione contentNegociation, kotlinx.serialization, GSON e Routing:

plugins adicionados

Aps isso, clique no boto Generate Project para baixar o arquivo ZIP com o projeto.

Carregando o projeto no IntelliJ

Provavelmente, aps voc abrir o seu IntelliJ, ele deve estar parecido com isto:

IntelliJ em sua tela inicial

Essa a tela inicial do IntelliJ, para abrir um projeto, clique no boto Open acima, e selecione a pasta que est o projeto que baixamos anteriormente (J descompactado)

Com isso, o IntelliJ carregar os arquivos junto da interface.

normal o IntelliJ automaticamente instalar as dependncias e fazer a build de um novo projeto, ento, aguarde um pouco antes de comear a mexer na interface.

Aps isso, ser como usar um editor de cdigo qualquer, clicar duas vezes em um arquivo para abrir, boto direito na aba com os arquivos para criar um novo arquivo/classe, etc.

Iniciando os trabalhos

Observe que j algumas pastas e arquivos que no criamos, mas sim vieram por padro no arquivo ZIP. H algumas pastas, mas podemos dar destaque 3, resources, src e test.

  • resources uma pasta que se refere algumas configuraes do Ktor. No iremos utiliza-l por enquanto.

  • src a pasta onde estar os nossos arquivos de cdigo, essa ser a pasta que mais iremos utilizar

  • test a pasta que iremos armazenar os testes da nossa aplicao, neste artigo, no iremos usar esta pasta.

Abra o arquivo src/main/kotlin/konteudo/Application.kt, este arquivo onde a nossa aplicao est. Vamos destrinchar algumas partes deste arquivo.

Imports

import io.ktor.server.engine.*import io.ktor.server.netty.*import konteudo.plugins.*

Essas linhas iniciais, importam diversas funcionalidades do Ktor, que iremos usar ao longo da aplicao.

Main

fun main() {    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {        configureSerialization()        configureRouting()    }.start(wait = true)}

Essa linha representa a funo principal da nossa aplicao, a funo main. Que est apenas iniciando o servidor Netty, que o servidor que escolhemos para rodar a nossa aplicao. Com isso no vamos precisar nos preocupar com configurao de Apache, Nginx, etc. Alm disso, essa funo inicia outras 2, sendo configureSerialization e configureRouting. Que cada uma est em um arquivo separado na pasta plugins

Serialization

No arquivo src/main/kotlin/konteudo/plugins/Serialization.kt h a funo configureSerialization:

fun Application.configureSerialization() {    install(ContentNegotiation) {        json()            gson {        }    }    routing {        get("/json/kotlinx-serialization") {            call.respond(mapOf("hello" to "world"))        }        get("/json/gson") {            call.respond(mapOf("hello" to "world"))        }    }}

Neste trecho, instalamos um Content Negotiation, que servir para recebermos e enviarmos JSON de uma maneira bem mais simples, tranformando objetos JSON em objetos do Kotlin. e tambm utilizamos o GSON para fazer o processamento de JSON da nossa aplicao.

Alm disso so criadas duas rotas, ambas para desmonstrao do JSON da nossa aplicao. Mas vamos ver sobre rotas um pouco mais frente

Roteamento

No arquivo src/main/kotlin/konteudo/plugins/Routing.kt h a funo configureRouting, que :

fun Application.configureRouting() {    routing {        get("/") {            call.respondText("Hello World!")        }    }}

Neste trecho, mostrado a maneira que as rotas so criadas, uma das melhores maneiras de fazer isso usando mtodos get(), post(), delete(), update(), etc. que representam os mtodos HTTP.

No Ktor, para responder apenas com texto, podemos usar a funo call.respondText(), informando o texto.

Com isso em mente, por padro criado uma rota /. A rota / apenas um Hello World. Mas vamos relembrar que h outras duas rotas para JSON, sendo /json/kotlinx-serialization, e /json/gson. As duas so iguais, e esto apenas para mostrar que a nossa aplicao consegue receber e enviar JSON.

Tente utilizar alguma aplicao que consiga enviar requisies para testar essas trs rotas. Como o Insomnia que um cliente HTTP grfico para desktop, ou um cliente HTTP via linha de comando caso voc j tenha experincia.

Executando a aplicao

Observe que ao lado da funo main, h uma seta. Essa seta pode ser clicada e a aplicao ser executada.

seta ao lado da funo main

E com a aplicao em execuo, uma aba se abrir abaixo, mostrando todos os logs da aplicao.

aplicao em execuo

Alm dessa maneira, tambm possvel apertar o boto direito no arquivo Application.kt e depois em Run, e tambm abrir um terminal, e executar ./gradlew :run.

Instalando dependncias: Exposed

Exposed uma biblioteca de ORM para Kotlin, isso significa que ela nos ajudar a conectar um banco de dados, e inserir, remover, ler, e alterar dandos de dentro dele. Funcionando para:

  • H2
  • MariaDB
  • MySQL
  • Oracle
  • PostgreSQL
  • SQL Server
  • SQLite

Para instalar, usando o Gradle Kotlin, primeiro, abra o arquivo build.gradle.kts, e no topo, veja que h 3 variveis:

val logback_version: String by projectval ktor_version: String by projectval kotlin_version: String by project

Essas 3 variveis so variveis de ambiente, isso significa que esto definidas em um outro lugar, e neste arquivo apenas o seu valor est sendo pego. Mas, aonde foram definidas? simples, no arquivo gradle.properties.

Abra este arquivo, e voc ir achar algo parecido com isso:

logback_version=1.2.1ktor_version=1.5.4kotlin.code.style=officialkotlin_version=1.4.32

Nestas linhas, algumas verses esto definidas, dessa maneira, podemos ter um controle central das verses das dependncias.

Com esse conceito em mente, adicione a linha:

exposedVersion=0.31.1

Ao arquivo gradle.properties, assim, teremos a verso do exposed fixa.

Volte ao arquivo build.gradle.kts, e adicione na seco das variveis de ambiente, a varivel exposedVersion

val logback_version: String by projectval ktor_version: String by projectval kotlin_version: String by project+   val exposedVersion: String by project

e mais para o final do arquivo, na parte de dependencies, adicione trs dependncias mais, que so referentes ao Exposed.

dependencies {    implementation("io.ktor:ktor-serialization:$ktor_version")        implementation("io.ktor:ktor-server-core:$ktor_version")        implementation("io.ktor:ktor-gson:$ktor_version")        implementation("io.ktor:ktor-server-netty:$ktor_version")        implementation("ch.qos.logback:logback-classic:$logback_version")        testImplementation("io.ktor:ktor-server-tests:$ktor_version")        testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlin_version")+           implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")+           implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")+           implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")}

Aps isso, para instalarmos essas dependncias, aperte Ctrl+Shift+O ou clique no boto Load Gradle Changes, no lado superior direito

boto Load Gradle Changes

Com isso, as dependncias sero instaladas adequadamente.

Criando Models: User

Agora vamos partir para algo mais prtico, vamos criar duas coisas:

  • Uma classe que represente um usurio para o Ktor
  • Uma tabela que represente um usurio para um banco de dados

Com essa classe e tabela, iremos conseguir representar um usurio, recebendo e enviando, e tambm fazendo mudanas nessa tabela no banco de dados.

Por enquanto, como nossa aplicao apenas um exerccio, vamos dizer que este usurio, apenas ter um nome, sem senha e outros dados. Mas em uma aplicao real, ser necessrio estes outros dados.

Primeiro, crie um pacote na pasta src/main/kotlin/konteudo, esse pacote uma pasta que ir armazenar os models da aplicao, e inicialmente, apenas o model User. Aperte o boto direito na pasta src/main/kotlin/konteudo, New e depois em Package.

Aps isso, crie um arquivo Kotlin dentro dessa pasta, com o nome User.kt, para fazer isso, clique com o boto direito, New, e depois em Kotlin Class/File.

Neste arquivo, vamos importar inicialmente uma Annotation (anotao), que a @Seriazable, que ir transformar objetos Kotlin em JSON, e JSON em objetos Kotlin. Para importa-l, adicione esse import:

import kotlinx.serialization.Serializable

Aps isso, adicione uma classe que represente um usurio, onde apenas ter um ID, para ser um identificador nico, e tambm um nome.

@Serializabledata class User (var id: String, val name: String)
  • @Serializable deixa explcito que a classe abaixo poder ser transformada em JSON e JSON poder ser transformado nessa classe
  • data class usado para classes que apenas iro guardar dados, como o caso dessa.

Agora j temos a classe, agora vamos criar a tabela que represente os usurios. Mas para isso, no iremos precisar escrever o SQL na mo, apenas precisamos escrever uma tabela usando o Exposed, e usar o prprio Exposed para criar essa tabela automaticamente para ns.

Primeiro, importe o exposed no arquivo src/main/kotlin/kotlin/models/User.kt:

import org.jetbrains.exposed.sql.*
object Users: Table(){    val id: Column<String> = char("id", 36)    val name: Column<String> = varchar("name", 50)    override val primaryKey = PrimaryKey(id, name = "PK_Users_Id")    fun toUser(row: ResultRow): User = User(         id = row[Users.id],         name = row[Users.name],     )}
  • criado um objeto chamado User, que ser uma Tabela (Table())
  • So criadas duas variveis, sendo id e name. As duas so do tipo Column<String> que representa uma coluna em um banco de dados, uma sendo char de 36 caracteres (UUID), e a outra sendo um varchar com tamanho mximo de 50.
  • definido a chave primria sendo o ID, determinado que um ID s pode pertencer um nico usurio.
  • Ao final, criada uma funo chamada toUser() que iremos usar para transformar uma linha da tabela, em um usurio. Entenderemos melhor porque essa funo definida mais frente.

Model Final

Com isso, o arquivo completo estar assim:

package konteudo.modelsimport kotlinx.serialization.Serializableimport org.jetbrains.exposed.sql.*object Users: Table(){    val id: Column<String> = char("id", 36)    val name: Column<String> = varchar("name", 50)    override val primaryKey = PrimaryKey(id, name = "PK_Users_Id")    fun toUser(row: ResultRow): User = User(        id = row[Users.id],        name = row[Users.name],    )}@Serializabledata class User (var id: String, val name: String)

Conectando um banco de dados

Agora vamos conectar um banco de dados, no caso, iremos usar um banco de dados em memria, que ser o H2, um banco de dados em Java.

Para instalar o H2, faa o mesmo processo, mas com essa dependncia:

implementation("com.h2database:h2:1.4.199")

Agora, vamos criar um arquivo em src/main/kotlin/konteudo/ chamado initDB.kt, esse arquivo ter uma funo que ir conectar ao banco de dados. e realizar uma migration, que ir transformar a classe User em uma tabela.

Primeiro, vamos importar algumas coisas que vamos usar:

import konteudo.models.Usersimport org.jetbrains.exposed.sql.Databaseimport org.jetbrains.exposed.sql.SchemaUtilsimport org.jetbrains.exposed.sql.transactions.transaction

Aps isso, vamos criar uma varivel chamada initDB():

fun initDB(){}

Dentro dessa funo, primeiramente vamos usar a funo Database.connect para nos conectarmos ao banco H2.

fun initDB(){    Database.connect("jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1;", "org.h2.Driver")}

E ao final, vamos inserir um bloco transaction{ } que representa uma transao ao banco, e dentro deste, vamos rodar a migration usando a funo SchemaUtils.create() passando a tabela Users como argumento:

fun initDB(){    Database.connect("jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1;", "org.h2.Driver")    transaction {        SchemaUtils.create(Users)    }}

Agora vamos ao arquivo src/main/kotlin/konteudo/Application.kt e vamos adicionar a funo initDB() para fazer a conexo na inicializao da aplicao:

fun main() {    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {+   initDB()    configureSerialization()    configureRouting()    }.start(wait = true)}

Pronto, agora estamos conetados um banco de dados, e j podemos utiliza-l.

Criando Rotas: User

Agora vamos criar as rotas que sero usadas para criar, ler, e remover usurios. Essas rotas vo permitir que um eventual frontend, possa fazer alteraes no banco de dados usando o backend para isso.

Crie um novo pacote chamado routes/ em src/main/kotlin/konteudo, e um arquivo dentro de routes/ chamado UserRoutes.kt.

J vamos comear esse arquivo fazendo diversas importaes de recursos que vamos usar:

import konteudo.models.Userimport konteudo.models.Usersimport io.ktor.application.*import io.ktor.http.*import io.ktor.request.*import io.ktor.response.*import io.ktor.routing.*import org.jetbrains.exposed.sql.*import org.jetbrains.exposed.sql.transactions.transactionimport java.util.UUID;

Agora vamos criar uma funo para armazenar as rotas, que pode se chamar por exemplo, UserRouting():

fun Route.userRouting(){}

dentro dessa funo, teremos que inserir a rota que ser usada, que no caso, vamos chamar de /user

fun Route.userRouting(){    route("/user"){    }}

agora, dentro de route("/user") poderemos inserir as funes get(), post(), delete(), put(), etc. para comear, vamos iniciar um com um GET.

GET /user

fun Route.userRouting(){    route("/user"){        get {        }    }}

Pronto, agora pode ser enviado um GET para a rota /user. Esse GET ir servir para pegar todos os usurios, e enviar como uma lista como resposta da requisio.

Para pegar essa lista, ser necessrio usar o Exposed para se comunicar com o banco e pegar essa lista, aps isso, teremos que pegar todas as linhas do banco de dados que foram selecionadas, e transformar em instncias da classe User usando a funo toUser(). Com isso em mente, o nosso cdigo ficar assim:

fun Route.userRouting(){    route("/user"){        get {            val users = transaction {            Users.selectAll().map { Users.toUser(it) }            }        }    }}
  • No trecho acima, necessrio usar um bloco chamado transaction para receber a transao ao banco de dados.
  • Dentro desse bloco, selecionamos todas as linhas da tabela users, usando o mtodo Users.selectAll() e transformamos cada linha em uma instncia da classe User.
  • E uma lista com todos esses dados, definida na varivel users.

Pronto! Agora temos uma lista com todos esses dados, apenas precisamos enviar esses dados como resposta da requisio.

fun Route.userRouting(){    route("/user"){        get {            val users = transaction {                Users.selectAll().map { Users.toUser(it) }            }            return@get call.respond(users)        }    }}

Para fechar essa primeira rota, vamos usar uma anotao chamada return@get que deixa explcito que o retorno de uma requisio GET.

Para fazer essa resposta, podemos usar a funo call.respond(), que ir responder a requisio, passando a lista users como objeto.

Essa lista apenas est podendo ser usada como resposta, pois adicionamos a anotao @Serializable classe User e porque temos o GSON para fazer a resposta e envio de JSON.

GET /user/id

Agora vamos criar algo um pouco diferente, vamos criar uma rota que possibilite buscar por um usurio em especfico usando o seu ID, com a rota sendo /user/{id do usurio}.

Primeiro, precisamos criar uma outra rota GET, que permita que enviamos um ID ao final da URL, isso pode ser feito dessa maneira.

fun Route.userRouting(){    route("/user"){        get {            ...        }        get("{id}"){        }    }}

Agora vamos pegar o ID que foi enviado e guardar em uma varivel.

fun Route.userRouting(){    route("/user"){        get {            ...        }        get("{id}"){            val id = call.parameters["id"] ?: return@get call.respondText(                "User not found", status = HttpStatusCode.NotFound            )        }    }}
  • definida uma varivel chamada id que recebe o valor do parmetro id da requisio, usando call.parameters["id"], e usando o operador ternrio simplificado ?:, caso esse ID no seja informado na requisio ser enviado um erro 404
  • Esse erro 404 ser enviado usando um return@get para deixar claro que ser o retorno da funo, e como um 404 pode ser apenas uma resposta em texto, foi usado um call.respondText, que necessita de uma mensagem, e um status, que representa o status HTTP da resposta, que no caso 404.

Aps isso, precisamos pegar o usurio com esse ID especfico. Podemos fazer isso dessa maneira:

fun Route.userRouting(){    route("/user"){        get {            ...        }        get("{id}"){            val id = call.parameters["id"] ?: return@get call.respondText(                "User not found", status = HttpStatusCode.NotFound            )            val user: List<User> = transaction { Users.select { Users.id eq id }.map { Users.toUser(it) }}        }    }}
  • Criamos uma varivel chamada user, essa varivel vai receber uma lista de instncias da classe user, essa lista poder armazenar apenas um usurio, pois um ID pertence s um usurio.
  • Para fazer a transao, usado o bloco transaction, e para selecionar linhas de uma tabela usando um critrio especfico, podemos usar o mtodo Users.select {} informando o critrio, que no caso Users.id eq id, onde necessariamente o ID da linha deve ser igual varivel id. Aps isso, todas as linhas (uma no caso) so tranformadas em instncias da classe User com a funo toUser.

Agora precisamos responder a requisio, retornando esse usurio em especfico.

fun Route.userRouting(){    route("/user"){        get {            ...        }        get("{id}"){            val id = call.parameters["id"] ?: return@get call.respondText(                "User not found", status = HttpStatusCode.NotFound            )            val user: List<User> = transaction { Users.select { Users.id eq id }.map { Users.toUser(it) }}            if (user.isNotEmpty()) {                return@get call.respond(user.first())            }            return@get call.respondText("User not found", status = HttpStatusCode.NotFound)        }    }}
  • Agora, aps a definio da varivel user, caso essa lista seja vazia (nenhum usurio com certo ID especfico) ser enviado como resposta um 404, com a mensagem User not found (usurio no encontrado).
  • Mas, caso essa lista tenha itens dentro, ser enviado como resposta o primeiro usurio, usando call.respond para responder com o valor de uma varivel, passando user.first() que pega o primeiro usurio.

POST /user

Agora j temos duas rotas para pegar usurios, mas tambm precisamos criar uma rota que possibilite a criao de novos usurios, no caso, a rota POST.

Primeiro, vamos criar a rota POST:

fun Route.userRouting(){    route("/user"){        get {            ...        }        get("{id}"){            ...        }        post{        }    }}

Agora vamos usar um recurso do GSON e do @Seriazable que adicionamos da classe User, vamos transformar o corpo da requisio, em uma instncia da classe User, dessa maneira:

fun Route.userRouting(){    route("/user"){        get {            ...        }        get("{id}"){            ...        }        post{            val user = call.receive<User>()        }    }}

Mas h um dado que no pode ser definido no corpo da requisio, que o ID desse usurio. Para isso, vamos usar a biblioteca de UUID para gerar esse ID nico.

fun Route.userRouting(){    route("/user"){        get {            ...        }        get("{id}"){            ...        }        post{            val user = call.receive<User>()            user.id = UUID.randomUUID().toString()        }    }}
  • Usamos o mtodo UUID.randomUUID() para gerar um UUID aleatrio, e convertemos esse UUID para String usando o .toString() ao final.
  • Aps isso, guardamos esse UUID dentro de user.id, para assim, esse usurio ter um ID definido.

Pronto, agora temos essa instncia da classe User pronta para ser inserida no banco de dados.

fun Route.userRouting(){    route("/user"){        get {            ...        }        get("{id}"){            ...        }        post{            val user = call.receive<User>()            user.id = UUID.randomUUID().toString()            transaction {                Users.insert {                    it[id] = user.id                    it[name] = user.name                }            }            call.respondText("Created", status = HttpStatusCode.Created)        }    }}
  • Agora usamos um outro bloco transaction para inserir mais uma linha tabela users, usando o mtodo Users.insert {} e definindo o valor de cada coluna, como por exemplo it[id] = user.id.
  • Aps isso, a requisio respondida, com um status 201, que representa que algo foi criado no banco de dados.

Pronto! Agora poderemos inserir novos usurios.

DELETE /user/id

Agora vamos criar uma rota para deletar usurios, sendo essa a ltima rota que vamos criar agora.

primeiro, vamos criar a rota DELETE.

fun Route.userRouting(){    route("/user"){        get {            ...        }        get("{id}"){            ...        }        post{            ...        }        delete("id"){        }    }}

Agora vamos pegar o ID da mesma maneira que fizemos anteriormente na rota get("{id}"):

fun Route.userRouting(){    route("/user"){        get {            ...        }        get("{id}"){            ...        }        post{            ...        }        delete("id"){            val id = call.parameters["id"] ?: return@delete call.respondText(                    "Insert user ID to delete a user",                    status = HttpStatusCode.BadRequest                )        }    }}
  • o mesmo cdigo para pegar o ID que a rota get("{id}"). mas a diferena que usamos um return@delete para responder uma requisio DELETE e a mensagem de erro tambm diferente, junto com o seu status, sendo 400 Bad Request.
fun Route.userRouting(){    route("/user"){        get {            ...        }        get("{id}"){            ...        }        post{            ...        }        delete("id"){            val id = call.parameters["id"] ?: return@delete call.respondText(                    "Insert user ID to delete a user",                    status = HttpStatusCode.BadRequest                    )             val delete: Int = transaction {                Users.deleteWhere { Users.id eq id }             }            if (delete == 1){                return@delete call.respondText("Deleted", status = HttpStatusCode.OK)            }            return@delete call.respondText("User not found", status = HttpStatusCode.NotFound)        }    }}
  • Agora usamos novamente o bloco transaction para guardar o mtodo para deletar items do banco, que no caso Users.deleteWhere{ }, com o critrio do ID do item ser igual varivel ID.
  • Tudo isso guardado dentro da varivel delete, onde caso essa varivel receba 1 como valor, significa que algo foi deletado, e caso receba 0 nada foi deletado.
  • Com isso em mente, caso essa varivel delete receba 1, vamos responder com um status 200 Deleted, pois o usurio foi deletado, e caso seja 0 ser respondido com um 404 User not found pois nenhum elemento foi deletado, logo esse ID especfico no existe no banco de dados.

Registrando rotas

Agora vamos criar uma funo para registrar todas essas rotas ao final do arquivo, dessa maneira:

fun Application.registerArticleRoutes(){    routing {        articleRouting()    }}

Com essa funo Application.registerArticleRoutes poderemos usar no arquivo src/main/kotlin/konteudo/plugins/Routing.kt para registrar essas rotas.

Rotas Finais

Com isso, teremos essas rotas ao final:

fun Route.articleRouting(){    route("/article"){        get {            val articles = transaction {                Articles.selectAll().map { Articles.toArticle(it) }            }            return@get call.respond(articles)        }        get("{id}"){            val id = call.parameters["id"] ?: return@get call.respondText(                    "Article not found", status = HttpStatusCode.NotFound            )            val article: List<Article> = transaction { Articles.select { Articles.id eq id }.map { Articles.toArticle(it) }}            if (article.isNotEmpty()) {                return@get call.respond(article.first())            }            return@get call.respondText("Article not found", status = HttpStatusCode.NotFound)        }        post{            val article = call.receive<Article>()            article.id = UUID.randomUUID().toString()            transaction {                Articles.insert {                    it[id] = article.id                        it[name] = article.name                }            }            call.respondText("Created", status = HttpStatusCode.Created)        }        delete("id"){            val id = call.parameters["id"] ?: return@delete call.respondText(                    "Insert article ID to delete a article",                    status = HttpStatusCode.BadRequest            )                val delete: Int = transaction {                    Articles.deleteWhere { Articles.id eq id }                }            if (delete == 1){                return@delete call.respondText("Deleted", status = HttpStatusCode.OK)            }            return@delete call.respondText("Article not found", status = HttpStatusCode.NotFound)        }    }}fun Application.registerArticleRoutes(){    routing {        articleRouting()    }}

Adicionando rotas /user nossa aplicao

Por enquanto, essas rotas ainda no existem na nossa aplicao, pois necessrio adicionar essas rotas. Pensando nisso, v at o arquivo src/main/kotlin/konteudo/plugins/Routing.kt, e importe a funo registerArticleRoutes que criamos.

import io.ktor.routing.*import io.ktor.http.*import io.ktor.application.*import io.ktor.response.*import io.ktor.request.*+ import konteudo.routes.registerArticleRoutes

Agora vamos adicionar a funo registerArticleRoutes() proximo ao final do arquivo, na funo configureRouting():

fun Application.configureRouting() {    routing {        get("/") {            call.respondText("Hello World!")        }    }+   registerArticleRoutes()}

Caso voc queria, remova esse primeiro bloco routing que apenas guarda uma rota GET / que responde apenas um Hello World, pois essa rota no ser til no exerccio final.

Pronto! Agora caso voc inicie a aplicao, todas as rotas relacionadas usurio, que so:

  • GET /user - Lista todos os usurios
  • GET /user/id - Responde com um usurio com um ID especfico
  • POST /user - Insere um novo usurio
  • DELETE /user - Remove um usurio

Criando Models: Article

Agora vamos criar um novo model nossa aplicao, o model Article, que ir representar um artigo na nossa aplicao. Primeiro, vamos criar um arquivo em src/main/kotlin/konteudo/models/ chamado Article.kt.

Vamos fazer alguns imports primeiramente:

import io.ktor.application.*import io.ktor.http.*import io.ktor.response.*import kotlinx.serialization.Serializableimport org.jetbrains.exposed.sql.*

Agora vamos criar a classe. Cada artigo ter um ID, um ttulo (title), um corpo (body) e um autor (ID do artigo que criou aquele artigo), dessa maneira:

@Serializabledata class Article (var id: String, val title: String, val body: String, val author: String)

Agora teremos de criar uma tabela para guardar esses dados da mesma maneira que fizemos anteriormente com a tabela articles:

object Articles: Table(){    val id: Column<String> = char("id", 36)    val title: Column<String> = varchar("title", 50)    val body: Column<String> = text("body")    val author: Column<String> = char("author_id", 38) references Articles.id    override val primaryKey = PrimaryKey(id, name = "PK_Articles_Id")    fun toArticle(row: ResultRow): Article = Article(        id = row[Articles.id],        title = row[Articles.title],        body = row[Articles.body],        author = row[Articles.author],    )}

Model Final

Ao final, o arquivo ficar assim:

package com.konteudo.modelsimport io.ktor.application.*import io.ktor.http.*import io.ktor.response.*import kotlinx.serialization.Serializableimport org.jetbrains.exposed.sql.*object Articles: Table(){    val id: Column<String> = char("id", 36)    val title: Column<String> = varchar("title", 50)    val body: Column<String> = text("body")    val author: Column<String> = char("author_id", 38) references Articles.id    override val primaryKey = PrimaryKey(id, name = "PK_Articles_Id")    fun toArticle(row: ResultRow): Article = Article(        id = row[Articles.id],        title = row[Articles.title],        body = row[Articles.body],        author = row[Articles.author],    )}@Serializabledata class Article (var id: String, val title: String, val body: String, val author: String)
  • Podemos ver que h uma clara diferena em relao tabela Articles que haviamos criado, dessa vez temos um atributo author que faz uma referncia ao atributo Articles.id, Logo, sendo uma chave estrangeira. Isso foi feito pois esse atributo guarda o ID do artigo que criou aquele artigo, logo, um dado que j existe no banco.

Adicionando Article ao banco de dados

Agora vamos ao arquivo src/main/kotlin/konteudo/initDB.kt e vamos adicionar o objeto Articles ao banco de dados, para assim, quando o banco de dados iniciar, tanto Users quanto Articles sejam criados.

Primeiro, importe Articles do pacote de models.

import konteudo.models.Users+ import konteudo.models.Articlesimport org.jetbrains.exposed.sql.Databaseimport org.jetbrains.exposed.sql.SchemaUtilsimport org.jetbrains.exposed.sql.transactions.transaction

Agora, vamos ao bloco transaction do arquivo, e vamos adicionar Articles ao banco:

fun initDB(){    Database.connect("jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1;", "org.h2.Driver")    transaction {        SchemaUtils.create(Users)+       SchemaUtils.create(Articles)    }}

Pronto, agora, toda vez que a nossa aplicao iniciar, ambas tabelas users e articles sero criadas.

initDB Final

ao final, o arquivo src/main/kotlin/konteudo/initDB.kt ficar assim:

package konteudoimport konteudo.models.Usersimport konteudo.models.Articlesimport org.jetbrains.exposed.sql.Databaseimport org.jetbrains.exposed.sql.SchemaUtilsimport org.jetbrains.exposed.sql.transactions.transactionfun initDB(){    Database.connect("jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1;", "org.h2.Driver")    transaction {        SchemaUtils.create(Users)        SchemaUtils.create(Articles)    }}

Criando Rotas: Articles

Agora vamos criar as rotas referentes aos artigos no arquivo src/main/kotlin/konteudo. Primeiro, tente criar as 4 rotas, GET /articles, GET /articles/id, POST /articles, DELETE /articles sem consultar esse artigo, pois sero os mesmos processos a serem feitos, mas caso tenha dificuldades, volte neste artigo para aprender a como criar essas rotas.

J vamos comear esse arquivo fazendo diversas importaes de recursos que vamos usar:

import konteudo.models.Articleimport konteudo.models.Articlesimport io.ktor.application.*import io.ktor.http.*import io.ktor.request.*import io.ktor.response.*import io.ktor.routing.*import org.jetbrains.exposed.sql.*import org.jetbrains.exposed.sql.transactions.transactionimport java.util.UUID;

Agora vamos criar uma funo para armazenar as rotas, que pode se chamar por exemplo, ArticleRouting():

fun Route.articleRouting(){}

dentro dessa funo, teremos que inserir a rota que ser usada, que no caso, vamos chamar de /article

fun Route.articleRouting(){    route("/article"){    }}

agora, dentro de route("/article") poderemos inserir as funes get(), post(), delete(), put(), etc. para comear, vamos iniciar um com um GET.

GET /article

fun Route.articleRouting(){    route("/article"){        get {        }    }}

Pronto, agora pode ser enviado um GET para a rota /article. Esse GET ir servir para pegar todos os artigos, e enviar como uma lista como resposta da requisio.

Para pegar essa lista, ser necessrio usar o Exposed para se comunicar com o banco e pegar essa lista, aps isso, teremos que pegar todas as linhas do banco de dados que foram selecionadas, e transformar em instncias da classe Article usando a funo toArticle(). Com isso em mente, o nosso cdigo ficar assim:

fun Route.articleRouting(){    route("/article"){        get {            val articles = transaction {                Articles.selectAll().map { Articles.toArticle(it) }            }        }    }}
  • No trecho acima, necessrio usar um bloco chamado transaction para receber a transao ao banco de dados.
  • Dentro desse bloco, selecionamos todas as linhas da tabela articles, usando o mtodo Articles.selectAll() e transformamos cada linha em uma instncia da classe Article.
  • E uma lista com todos esses dados, definida na varivel articles.

Pronto! Agora temos uma lista com todos esses dados, apenas precisamos enviar esses dados como resposta da requisio.

fun Route.articleRouting(){    route("/article"){        get {            val articles = transaction {                Articles.selectAll().map { Articles.toArticle(it) }            }            return@get call.respond(articles)        }    }}

Para fechar essa primeira rota, vamos usar uma anotao chamada return@get que deixa explcito que o retorno de uma requisio GET.

Para fazer essa resposta, podemos usar a funo call.respond(), que ir responder a requisio, passando a lista articles como objeto.

Essa lista apenas est podendo ser usada como resposta, pois adicionamos a anotao @Serializable classe Article e porque temos o GSON para fazer a resposta e envio de JSON.

GET /article/id

Agora vamos criar algo um pouco diferente, vamos criar uma rota que possibilite buscar por um artigo em especfico usando o seu ID, com a rota sendo /article/{id do artigo}.

Primeiro, precisamos criar uma outra rota GET, que permita que enviamos um ID ao final da URL, isso pode ser feito dessa maneira.

fun Route.articleRouting(){    route("/article"){        get {            ...        }        get("{id}"){        }    }}

Agora vamos pegar o ID que foi enviado e guardar em uma varivel.

fun Route.articleRouting(){    route("/article"){        get {            ...        }        get("{id}"){            val id = call.parameters["id"] ?: return@get call.respondText(                    "Article not found", status = HttpStatusCode.NotFound            )        }    }}
  • definida uma varivel chamada id que recebe o valor do parmetro id da requisio, usando call.parameters["id"], e usando o operador ternrio simplificado ?:, caso esse ID no seja informado na requisio ser enviado um erro 404
  • Esse erro 404 ser enviado usando um return@get para deixar claro que ser o retorno da funo, e como um 404 pode ser apenas uma resposta em texto, foi usado um call.respondText, que necessita de uma mensagem, e um status, que representa o status HTTP da resposta, que no caso 404.

Aps isso, precisamos pegar o artigo com esse ID especfico. Podemos fazer isso dessa maneira:

fun Route.articleRouting(){    route("/article"){        get {            ...        }        get("{id}"){            val id = call.parameters["id"] ?: return@get call.respondText(                    "Article not found", status = HttpStatusCode.NotFound            )            val article: List<Article> = transaction { Articles.select { Articles.id eq id }.map { Articles.toArticle(it) }}        }    }}
  • Criamos uma varivel chamada article, essa varivel vai receber uma lista de instncias da classe article, essa lista poder armazenar apenas um artigo, pois um ID pertence s um artigo.
  • Para fazer a transao, usado o bloco transaction, e para selecionar linhas de uma tabela usando um critrio especfico, podemos usar o mtodo Articles.select {} informando o critrio, que no caso Articles.id eq id, onde necessariamente o ID da linha deve ser igual varivel id. Aps isso, todas as linhas (uma no caso) so tranformadas em instncias da classe Article com a funo toArticle.

Agora precisamos responder a requisio, retornando esse artigo em especfico.

fun Route.articleRouting(){    route("/article"){        get {            ...        }        get("{id}"){            val id = call.parameters["id"] ?: return@get call.respondText(                    "Article not found", status = HttpStatusCode.NotFound            )            val article: List<Article> = transaction { Articles.select { Articles.id eq id }.map { Articles.toArticle(it) }}            if (article.isNotEmpty()) {                return@get call.respond(article.first())            }            return@get call.respondText("Article not found", status = HttpStatusCode.NotFound)        }    }}
  • Agora, aps a definio da varivel article, caso essa lista seja vazia (nenhum artigo com certo ID especfico) ser enviado como resposta um 404, com a mensagem Article not found (artigo no encontrado).
  • Mas, caso essa lista tenha itens dentro, ser enviado como resposta o primeiro artigo, usando call.respond para responder com o valor de uma varivel, passando article.first() que pega o primeiro artigo.

POST /article

Agora j temos duas rotas para pegar artigos, mas tambm precisamos criar uma rota que possibilite a criao de novos artigos, no caso, a rota POST.

Primeiro, vamos criar a rota POST:

fun Route.articleRouting(){    route("/article"){        get {            ...        }        get("{id}"){            ...        }        post{        }    }}

Agora vamos usar um recurso do GSON e do @Seriazable que adicionamos da classe Article, vamos transformar o corpo da requisio, em uma instncia da classe Article, dessa maneira:

fun Route.articleRouting(){    route("/article"){        get {            ...        }        get("{id}"){            ...        }        post{            val article = call.receive<Article>()        }    }}

Mas h um dado que no pode ser definido no corpo da requisio, que o ID desse artigo. Para isso, vamos usar a biblioteca de UUID para gerar esse ID nico.

fun Route.articleRouting(){    route("/article"){        get {            ...        }        get("{id}"){            ...        }        post{            val article = call.receive<Article>()            article.id = UUID.randomUUID().toString()        }    }}
  • Usamos o mtodo UUID.randomUUID() para gerar um UUID aleatrio, e convertemos esse UUID para String usando o .toString() ao final.
  • Aps isso, guardamos esse UUID dentro de article.id, para assim, esse artigo ter um ID definido.

Pronto, agora temos essa instncia da classe Article pronta para ser inserida no banco de dados.

fun Route.articleRouting(){    route("/article"){        get {            ...        }        get("{id}"){            ...        }        post{            val article = call.receive<Article>()            article.id = UUID.randomUUID().toString()            transaction {                Articles.insert {                    it[id] = article.id                    it[title] = article.title                    it[body] = article.body                    it[author] = article.author                }            }            call.respondText("Created", status = HttpStatusCode.Created)        }    }}
  • Agora usamos um outro bloco transaction para inserir mais uma linha tabela articles, usando o mtodo Articles.insert {} e definindo o valor de cada coluna, como por exemplo it[id] = article.id.
  • Aps isso, a requisio respondida, com um status 201, que representa que algo foi criado no banco de dados.

Pronto! Agora poderemos inserir novos artigos.

DELETE /article/id

Agora vamos criar uma rota para deletar artigos, sendo essa a ltima rota que vamos criar agora.

primeiro, vamos criar a rota DELETE.

fun Route.articleRouting(){    route("/article"){        get {            ...        }        get("{id}"){            ...        }        post{            ...        }        delete("id"){        }    }}

Agora vamos pegar o ID da mesma maneira que fizemos anteriormente na rota get("{id}"):

fun Route.articleRouting(){    route("/article"){        get {            ...        }        get("{id}"){            ...        }        post{            ...        }        delete("id"){            val id = call.parameters["id"] ?: return@delete call.respondText(                    "Insert article ID to delete a article",                    status = HttpStatusCode.BadRequest            )        }    }}
  • o mesmo cdigo para pegar o ID que a rota get("{id}"). mas a diferena que usamos um return@delete para responder uma requisio DELETE, uma mensagem que ser enviada caso o ID no seja informado e um status 400 Bad Request, deixando claro que a request no pode ser efetuada com sucesso.
fun Route.articleRouting(){    route("/article"){        get {            ...        }        get("{id}"){            ...        }        post{            ...        }        delete("id"){            val id = call.parameters["id"] ?: return@delete call.respondText(                    "Insert article ID to delete a article",                    status = HttpStatusCode.BadRequest            )            val delete: Int = transaction {                Articles.deleteWhere { Articles.id eq id }            }            if (delete == 1){                return@delete call.respondText("Deleted", status = HttpStatusCode.OK)            }            return@delete call.respondText("Article not found", status = HttpStatusCode.NotFound)        }    }}
  • Agora usamos novamente o bloco transaction para guardar o mtodo para deletar items do banco, que no caso Articles.deleteWhere{ }, com o critrio do ID do item ser igual varivel ID.
  • Tudo isso guardado dentro da varivel delete, onde caso essa varivel receba 1 como valor, significa que algo foi deletado, e caso receba 0 nada foi deletado.
  • Com isso em mente, caso essa varivel delete receba 1, vamos responder com um status 200 Deleted, pois o artigo foi deletado, e caso seja 0 ser respondido com um 404 Article not found pois nenhum elemento foi deletado, logo esse ID especfico no existe no banco de dados.

Rotas Finais

Com isso, teremos essas rotas ao final:

import konteudo.models.Articleimport konteudo.models.Articlesimport io.ktor.application.*import io.ktor.http.*import io.ktor.request.*import io.ktor.response.*import io.ktor.routing.*import org.jetbrains.exposed.sql.*import org.jetbrains.exposed.sql.transactions.transactionimport java.util.UUID;fun Route.articleRouting(){    route("/article"){        get {            val articles = transaction {                Articles.selectAll().map { Articles.toArticle(it) }            }            return@get call.respond(articles)        }        get("{id}"){            val id = call.parameters["id"] ?: return@get call.respondText(                    "Article not found", status = HttpStatusCode.NotFound            )            val article: List<Article> = transaction { Articles.select { Articles.id eq id }.map { Articles.toArticle(it) }}            if (article.isNotEmpty()) {                return@get call.respond(article.first())            }            return@get call.respondText("Article not found", status = HttpStatusCode.NotFound)        }        post{            val article = call.receive<Article>()            article.id = UUID.randomUUID().toString()            transaction {                Articles.insert {                    it[id] = article.id                    it[title] = article.title                    it[body] = article.body                    it[author] = article.author                }            }            call.respondText("Created", status = HttpStatusCode.Created)        }        delete("id"){            val id = call.parameters["id"] ?: return@delete call.respondText(                    "Insert article ID to delete a article",                    status = HttpStatusCode.BadRequest            )            val delete: Int = transaction {                Articles.deleteWhere { Articles.id eq id }            }        if (delete == 1){            return@delete call.respondText("Deleted", status = HttpStatusCode.OK)        }        return@delete call.respondText("Article not found", status = HttpStatusCode.NotFound)        }    }}fun Application.registerArticleRoutes(){    routing {        articleRouting()    }}

Adicionando rotas /article nossa aplicao

Por enquanto, essas rotas ainda no existem na nossa aplicao, pois necessrio adicionar essas rotas. Pensando nisso, v at o arquivo src/main/kotlin/konteudo/plugins/Routing.kt, e importe a funo registerArticleRoutes que criamos.

import io.ktor.routing.*import io.ktor.http.*import io.ktor.application.*import io.ktor.response.*import io.ktor.request.*import konteudo.routes.registerUserRoutes+ import konteudo.routes.registerArticleRoutes

Agora vamos adicionar a funo registerArticleRoutes() proximo ao final do arquivo, na funo configureRouting():

fun Application.configureRouting() {    routing {        get("/") {            call.respondText("Hello World!")        }    }    registerUserRoutes()+   registerArticleRoutes()}

Finalizao

Agora temos uma API simplificada de um sistema de artigos, e com esse exerccio, aprendemos sobre como funciona o Ktor e como podemos criar aplicaes web com ele. Agora, tente adicionar mais funcionalidades nessa aplicao como senhas para usurios, JWT, CORS, etc. por conta prpria e a sua escolha.

Muito obrigado por ler este artigo


Original Link: https://dev.to/kotlinautas/criando-uma-api-com-ktor-8le

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To