My earlier article launched Ktor and a few of its primary options for constructing net purposes. Now, we’ll increase the instance software developed in that article by including persistent knowledge and HTMX, which is able to present extra interactive views. This offers us a setup with lots of energy in a comparatively easy stack.
Please see the earlier article for the instance software code and setup. We’ll construct on that instance right here.
Add persistence to the Ktor-HTMX software
Step one towards making our software extra highly effective is so as to add persistent knowledge. The preferred approach to work together with an SQL database in Kotlin is with the Uncovered ORM framework. It offers us a few methods to work together with the database, utilizing both a DAO mapping or a DSL. Kotlin’s native syntax means the general really feel of utilizing the ORM mapping layer has much less overhead than others you might need encountered.
We’ll want so as to add a number of dependencies to our construct.gradle.kt
, along with these we have already got:
dependencies {
// present deps...
implementation("org.jetbrains.uncovered:exposed-core:0.41.1")
implementation("org.jetbrains.uncovered:exposed-jdbc:0.41.1")
implementation("com.h2database:h2:2.2.224")
}
You’ll discover we’ve included the uncovered core and JDBC libraries, in addition to a driver for the in-memory H2 database. We’ll use H2 as a easy persistence mechanism that may simply be converted to an exterior SQL database like Postgres afterward.
Add providers
To start out with, we’ll create a few easy providers that work together with a essential service, which talks to the database. Right here’s our QuoteSchema.kt
file to this point, which units up the database schema and gives service capabilities for interacting with it:
// src/essential/kotlin/com/instance/plugins/QuoteSchema.kt
package deal com.instance.plugins
import kotlinx.coroutines.*
import org.jetbrains.uncovered.sql.*
import org.jetbrains.uncovered.sql.transactions.transaction
object Quotes : Desk() {
val id: Column = integer("id").autoIncrement()
val quote = textual content("quote")
val creator = textual content("creator")
override val primaryKey = PrimaryKey(id, identify = "PK_Quotes_ID")
}
knowledge class Quote(val id: Int? = null, val quote: String, val creator: String)
class QuoteService {
droop enjoyable create(quote: Quote): Int = withContext(Dispatchers.IO) {
transaction {
Quotes.insert {
it[this.quote] = quote.quote
it[this.author] = quote.creator
} get Quotes.id
} ?: throw Exception("Unable to create quote")
}
droop enjoyable record(): Listing = withContext(Dispatchers.IO) {
transaction {
Quotes.selectAll().map {
Quote(
id = it[Quotes.id],
quote = it[Quotes.quote],
creator = it[Quotes.author]
)
}
}
}
}
There’s so much happening on this file, so let’s take it step-by-step. The very first thing we do is declare a Quotes
object that extends Desk
. Desk
is part of the Uncovered framework and lets us outline a desk within the database. It does lots of work for us based mostly on the 4 variables we outline: id
, quote
, creator
, and major key
. The id
ingredient will likely be auto-generated for an auto-increment major key, whereas the opposite two could have their acceptable column sorts (textual content
turns into string
, for instance, relying on the database’s dialect and driver).
Uncovered can also be good sufficient to solely generate the desk if it doesn’t exist already.
Subsequent, we declare an information class known as Quote
, utilizing the constructor model. Discover id
is marked as elective (since it will likely be auto-generated).
Then, we create a QuoteService
class with two suspendable capabilities: create
and record
. These are each interacting with the concurrent assist in Kotlin, utilizing the IO dispatcher. These strategies are optimized for IO-bound concurrency, which is suitable for database entry.
Inside every service technique, now we have a database transaction, which does the work of both inserting a brand new Quote
or returning a Listing
of Quote
s.
Routes
Now let’s make a Database.kt
file that pulls within the QuoteService
and exposes endpoints for interacting with it. We’ll want a POST
for creating quotes and a GET
for itemizing them.
//src/essential/kotlin/com/instance/plugins/Database.kt
package deal com.instance.plugins
import io.ktor.http.*
import io.ktor.server.software.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import java.sql.*
import kotlinx.coroutines.*
import org.jetbrains.uncovered.sql.*
import org.jetbrains.uncovered.sql.transactions.transaction
enjoyable Utility.configureDatabases() {
val database = Database.join(
url = "jdbc:h2:mem:check;DB_CLOSE_DELAY=-1",
person = "root",
driver = "org.h2.Driver",
password = "",
)
transaction {
SchemaUtils.create(Quotes)
}
val quoteService = QuoteService()
routing {
submit("/quotes") {
val parameters = name.receiveParameters()
val quote = parameters["quote"] ?: ""
val creator = parameters["author"] ?: ""
val newQuote = Quote(quote = quote, creator = creator)
val id = quoteService.create(newQuote)
name.reply(HttpStatusCode.Created, id)
}
get("/quotes") {
val quotes = quoteService.record()
name.reply(HttpStatusCode.OK, quotes)
}
}
}
We start by utilizing Database.join
from the Uncovered framework to create a database connection utilizing customary H2 parameters. Then, inside a transaction we create the Quotes schema, utilizing our Quotes
class we outlined in QuoteSchema.kt
.
Subsequent, we create two routes utilizing the syntax we developed in the primary stage of this instance and counting on the create
and record
capabilities and Quote
class from QuoteSchema
.
Don’t overlook to incorporate the brand new perform in Utility.kt
:
// src/essential/kotlin/com/instance/Utility.kt
package deal com.instance
import com.instance.plugins.*
import io.ktor.server.software.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
enjoyable essential(args: Array) {
io.ktor.server.netty.EngineMain.essential(args)
}
enjoyable Utility.module() {
configureTemplating()
//configureRouting()
set up(RequestLoggingPlugin)
configureDatabases()
}
Discover I’ve commented out the previous configureRouting()
name, so it received’t battle with our new routes.
To do a fast check of those routes, we will use the curl
command-line software. This line inserts a row:
$ curl -X POST -H "Content material-Kind: software/x-www-form-urlencoded" -H "Host: localhost:8080" -d "quote=FooBar.&creator=William+Shakespeare" http://localhost:8080/quotes
And this one outputs the present rows:
$ curl http://localhost:8080/quotes
Utilizing HTMX for interactive views
Now let’s soar proper into making a UI to work together with the providers utilizing HTMX. We wish a web page that lists the present quotes and a kind that we will use to submit a brand new quote. The quote will likely be dynamically inserted into the record on the web page, with no web page reload.
To attain these targets, we’ll want a route that pulls every thing on the outset after which one other route that accepts the shape POST
and returns the markup for the newly inserted quote. We’ll add these to the Database.kt
routes for simplicity.
Right here is the /quotes-htmx
web page that provides us the preliminary record and kind:
get("/quotes-htmx") {
val quotes = quoteService.record()
name.respondHtml {
head {
script(src = "https://unpkg.com/htmx.org@1.9.6") {}
}
physique {
h1 { +"Quotes (HTMX)" }
div {
id = "quotes-list"
quotes.forEach { quote ->
div {
p { +quote.quote }
p { +"― ${quote.creator}" }
}
}
}
kind(technique = FormMethod.submit, motion = "/quotes", encType = FormEncType.applicationXWwwFormUrlEncoded) {
attributes["hx-post"] = "/quotes"
attributes["hx-target"] = "#quotes-list"
attributes["hx-swap"] = "beforeend"
div {
label { +"Quote:" }
textInput(identify = "quote")
}
div {
label { +"Writer:" }
textInput(identify = "creator")
}
button(kind = ButtonType.submit) { +"Add Quote" }
}
}
}
}
First, we seize the record of quotes from the service. Then we begin outputting the HTML, starting with a head ingredient that features the HTMX library from a CDN. Subsequent, we open a physique
tag and render a title
(H1) ingredient adopted by a div
with the id
of quotes-list
. Discover that id
is dealt with as a name from contained in the div
block, as a substitute of as an attribute on div
.
Inside quotes-list
, we iterate over the quotes assortment and output a div
with every quote and creator. (Within the Specific model of this software, we used a UL and record gadgets. We may have finished the identical right here.)
After the record comes the shape, which units a number of non-standard attributes (hx-post
, hx-target
, and hx-swap
) on the attributes
assortment. These will likely be set on the output HTML kind ingredient.
Now all we’d like is a /quotes
route to simply accept the incoming quotes from POST
and reply with an HTML fragment that represents the brand new quote to be inserted into the record:
submit("/quotes") {
val parameters = name.receiveParameters()
val quote = parameters["quote"] ?: ""
val creator = parameters["author"] ?: ""
val newQuote = Quote(quote = quote, creator = creator)
val id = quoteService.create(newQuote)
val createdQuote = quoteService.learn(id)
name.respondHtml(HttpStatusCode.Created) {
physique{
div {
p { +createdQuote.quote }
p { +"― ${createdQuote.creator}" }
}
}
}
That is fairly easy. One wrinkle is that Kotlin’s HTML DSL doesn’t prefer to ship an HTML fragment, so now we have to wrap our quote markup in a physique
tag, which shouldn’t be there. (There’s a easy workaround we’re skipping for simplicity, present in this mission known as respondHtmlFragment). It appears possible that producing HTML fragments will ultimately turn out to be a normal a part of the HTML DSL.
Aside from that, we simply parse the shape and use the service to create a Quote
after which use the brand new Quote
to generate the response, which HTMX will use to replace the UI dynamically.
Conclusion
We went quick and lean with this instance, to discover the essence of Ktor. Nonetheless, now we have all the weather of a extremely performant and dynamic stack with out a lot overhead. As a result of Kotlin is constructed on prime of the JVM it offers you entry to every thing Java does. That, coupled with its highly effective union of object-oriented and useful programming, and DSL capabilities, makes Kotlin a compelling server-side language. You should use it for constructing purposes with conventional RESTful JSON endpoints, or with dynamic HTMX-powered UIs, as we’ve seen right here.
See my GitHub repository for the whole supply code for the Ktor-HTMX software instance.