Meu artigo anterior apresentou o Ktor e alguns de seus recursos básicos para a construção de aplicações web. Agora, expandiremos o aplicativo de exemplo desenvolvido nesse artigo adicionando dados persistentes e HTMX, que fornecerão visualizações mais interativas. Isso nos dá uma configuração com muito poder em uma pilha relativamente simples.

Consulte o artigo anterior para obter exemplos de código e configuração do aplicativo. Vamos desenvolver esse exemplo aqui.

Adicione persistência ao aplicativo Ktor-HTMX

O primeiro passo para tornar nosso aplicativo mais poderoso é adicionar dados persistentes. A maneira mais popular de interagir com um banco de dados SQL em Kotlin é com a estrutura Exposed ORM. Isso nos dá algumas maneiras de interagir com o banco de dados, usando um mapeamento DAO ou DSL. A sintaxe nativa do Kotlin significa que a sensação geral de usar a camada de mapeamento ORM tem menos sobrecarga do que outras que você possa ter encontrado.

Precisaremos adicionar algumas dependências ao nosso build.gradle.ktalém daqueles que já temos:


dependencies {
  // existing deps...
    implementation("org.jetbrains.exposed:exposed-core:0.41.1")
  implementation("org.jetbrains.exposed:exposed-jdbc:0.41.1") 
  implementation("com.h2database:h2:2.2.224")
}

Você notará que incluímos o núcleo exposto e as bibliotecas JDBC, bem como um driver para o banco de dados H2 na memória. Usaremos H2 como um mecanismo de persistência simples que pode ser facilmente transferido para um banco de dados SQL externo como o Postgres posteriormente.

Adicionar serviços

Para começar, criaremos alguns serviços simples que interagem com um serviço principal, que se comunica com o banco de dados. Aqui está o nosso QuoteSchema.kt arquivo até agora, que configura o esquema do banco de dados e fornece funções de serviço para interagir com ele:


// src/main/kotlin/com/example/plugins/QuoteSchema.kt
package com.example.plugins

import kotlinx.coroutines.*
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction

object Quotes : Table() {
    val id: Column = integer("id").autoIncrement()
    val quote = text("quote")
    val author = text("author")

    override val primaryKey = PrimaryKey(id, name = "PK_Quotes_ID")
}

data class Quote(val id: Int? = null, val quote: String, val author: String)

class QuoteService {
    suspend fun create(quote: Quote): Int = withContext(Dispatchers.IO) {
      transaction {
        Quotes.insert {
          it(this.quote) = quote.quote
          it(this.author) = quote.author
        } get Quotes.id
      } ?: throw Exception("Unable to create quote")
    }
    suspend fun list(): List = withContext(Dispatchers.IO) {
        transaction {
            Quotes.selectAll().map {
                Quote(
                    id = it(Quotes.id),
                    quote = it(Quotes.quote),
                    author = it(Quotes.author)
                )
            }
        }
    }
}

Há muita coisa acontecendo neste arquivo, então vamos dar um passo a passo. A primeira coisa que fazemos é declarar um Quotes objeto que se estende Table. Table faz parte do framework Exposed e nos permite definir uma tabela no banco de dados. Faz muito trabalho para nós com base nas quatro variáveis ​​que definimos: id, quote, authore primary key. O id elemento será gerado automaticamente para uma chave primária de incremento automático, enquanto os outros dois terão seus tipos de coluna apropriados (text torna-se stringpor exemplo, dependendo do dialeto e driver do banco de dados).

O Exposed também é inteligente o suficiente para gerar a tabela apenas se ela ainda não existir.

A seguir, declaramos uma classe de dados chamada Quoteusando o estilo do construtor. Perceber id está marcado como opcional (já que será gerado automaticamente).

Então, criamos um QuoteService classe com duas funções suspendíveis: create e list. Ambos estão interagindo com o suporte simultâneo em Kotlin, usando o IO Dispatcher. Esses métodos são otimizados para simultaneidade vinculada a E/S, o que é apropriado para acesso ao banco de dados.

Dentro de cada método de serviço, temos uma transação de banco de dados, que faz o trabalho de inserir um novo Quote ou devolvendo um List de QuoteS.

Rotas

Agora vamos fazer um Database.kt arquivo que puxa o QuoteService e expõe endpoints para interagir com ele. Precisaremos de um POST para criar cotações e um GET por listá-los.


//src/main/kotlin/com/example/plugins/Database.kt 
package com.example.plugins

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.sql.*
import kotlinx.coroutines.*
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction

fun Application.configureDatabases() {
    val database = Database.connect(
        url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
        user = "root",
        driver = "org.h2.Driver",
        password = "",
    )
    transaction {
        SchemaUtils.create(Quotes)
    }
    val quoteService = QuoteService() 
    routing {
        post("/quotes") {
          val parameters = call.receiveParameters()
          val quote = parameters("quote") ?: ""
          val author = parameters("author") ?: ""
 
          val newQuote = Quote(quote = quote, author = author) 
 
          val id = quoteService.create(newQuote)
          call.respond(HttpStatusCode.Created, id)
        }
        get("/quotes") {
            val quotes = quoteService.list()
            call.respond(HttpStatusCode.OK, quotes)
        }
    }
}

Começamos usando Database.connect da estrutura Exposed para criar uma conexão de banco de dados usando parâmetros H2 padrão. Então, dentro de uma transação criamos o esquema Quotes, usando nosso Quotes classe que definimos em QuoteSchema.kt.

A seguir, criamos duas rotas usando a sintaxe que desenvolvemos na primeira etapa deste exemplo e contando com o create e list funções e Quote aula de QuoteSchema.

Não se esqueça de incluir a nova função em Application.kt:


// src/main/kotlin/com/example/Application.kt 
package com.example

import com.example.plugins.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*


fun main(args: Array) {
    io.ktor.server.netty.EngineMain.main(args)
}

fun Application.module() {

  configureTemplating()
  //configureRouting()
  install(RequestLoggingPlugin)

  configureDatabases()
}

Observe que comentei o antigo configureRouting() ligue, para que não entre em conflito com nossas novas rotas.

Para fazer um teste rápido dessas rotas, podemos usar o curl ferramenta de linha de comando. Esta linha insere uma linha:


$ curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -H "Host: localhost:8080" -d "quote=FooBar.&author=William+Shakespeare" http://localhost:8080/quotes

E este gera as linhas existentes:


$ curl http://localhost:8080/quotes

Usando HTML para visualizações interativas

Agora vamos direto para a criação de uma UI para interagir com os serviços usando HTMX. Queremos uma página que liste as cotações existentes e um formulário que possamos usar para enviar uma nova cotação. A cotação será inserida dinamicamente na lista da página, sem recarregar a página.

Para atingir estes objetivos, precisaremos de uma rota que desenhe tudo no início e depois de outra rota que aceite a forma POST e retorna a marcação da cotação recém-inserida. Vamos adicioná-los ao Database.kt rotas para simplificar.

Aqui está o /quotes-htmx página que nos fornece a lista inicial e o formulário:


get("/quotes-htmx") {
        val quotes = quoteService.list()    
        call.respondHtml {
          head {
            script(src = "https://unpkg.com/[email protected]") {} 
          }
        body {
          h1 { +"Quotes (HTMX)" }
          div {
            id = "quotes-list"
            quotes.forEach { quote ->
              div {
                p { +quote.quote }
                p { +"― ${quote.author}" }
              }
            }
          }
          form(method = FormMethod.post, action = "/quotes", encType = FormEncType.applicationXWwwFormUrlEncoded) {
            attributes("hx-post") = "/quotes"
            attributes("hx-target") = "#quotes-list"
            attributes("hx-swap") = "beforeend" 
            div {
              label { +"Quote:" }
              textInput(name = "quote")
            }
            div {
              label { +"Author:" }
              textInput(name = "author")
            }
            button(type = ButtonType.submit) { +"Add Quote" }
          }
        }
      }
    }

Primeiro, pegamos a lista de cotações do serviço. Então começamos a gerar o HTML, começando com um elemento head que inclui a biblioteca HTMX de um CDN. A seguir, abrimos um body marcar e renderizar um title (H1) elemento seguido por um div com o id de quotes-list. Observe que id é tratado como uma chamada de dentro do div bloco, em vez de como um atributo em div.

Dentro quotes-listiteramos sobre a coleção de cotações e geramos um div com cada citação e autor. (Na versão Express deste aplicativo, usamos um UL e itens de lista. Poderíamos ter feito o mesmo aqui.)

Após a lista vem o formulário, que define vários atributos não padrão (hx-post, hx-targete hx-swap) no attributes coleção. Eles serão definidos no elemento de formulário HTML de saída.

Agora tudo o que precisamos é de um /quotes rota para aceitar as cotações recebidas de POST e responda com um fragmento HTML que representa a nova citação a ser inserida na lista:


post("/quotes") {
      val parameters = call.receiveParameters()
      val quote = parameters("quote") ?: ""
      val author = parameters("author") ?: ""
      val newQuote = Quote(quote = quote, author = author)
      val id = quoteService.create(newQuote)
      val createdQuote = quoteService.read(id) 
      call.respondHtml(HttpStatusCode.Created) { 
        body{
        div {
          p { +createdQuote.quote }
          p { +"― ${createdQuote.author}" }
        }
    }
  }

Isso é bastante simples. Um problema é que o HTML DSL do Kotlin não gosta de enviar um fragmento HTML, então temos que agrupar nossa marcação de cotação em um body tag, que não deveria estar lá. (Há uma solução alternativa simples que estamos ignorando para simplificar, encontrada neste projeto chamada respondHtmlFragment). Parece provável que a geração de fragmentos HTML eventualmente se tornará uma parte padrão do HTML DSL.

Fora isso, apenas analisamos o formulário e usamos o serviço para criar um Quote e então use o novo Quote para gerar a resposta, que o HTMX usará para atualizar a UI dinamicamente.

Conclusão

Fomos rápidos e enxutos com este exemplo, para explorar a essência do Ktor. No entanto, temos todos os elementos de uma pilha dinâmica e de alto desempenho sem muita sobrecarga. Como o Kotlin é construído sobre a JVM, ele oferece acesso a tudo o que o Java faz. Isso, juntamente com sua poderosa união de programação funcional e orientada a objetos e recursos DSL, torna o Kotlin uma linguagem atraente do lado do servidor. Você pode usá-lo para criar aplicativos com endpoints JSON RESTful tradicionais ou com UIs dinâmicas baseadas em HTMX, como vimos aqui.

Consulte meu repositório GitHub para obter o código-fonte completo do exemplo do aplicativo Ktor-HTMX.