Hypermedia Systems + Kotlin
HTMX has been getting a lot of attention, and I like how little it asks me to write. I think almost 90% of web use cases need no more than HTML and a little JavaScript for interaction. HTMX1 is a JavaScript library based on HATEOAS (Hypermedia as the Engine of Application State)2. In this approach, the server sends content such as HTML that the browser can display and interact with. It feels familiar if you have built sites with jQuery, PHP, or model-view-controller (MVC) frameworks.
Letting the server describe the page
In HATEOAS, the client does not need to know the application's data in advance. Client and server communicate through an agreed interface, described in documentation or an interface language. For page updates, this has a few benefits:
- The server holds the application state and sends its current representation to the browser, reducing the state the client has to manage.
- The server can send part of a page and specify which element to replace. This gives the feel of a single-page application (SPA) with little client-side scripting.
- Frontend and backend developers can work in the same codebase at the same time.
- There can be much less code to maintain, as the published React-to-HTMX ports show34.
Search results as you type
Suppose we want to show search results shortly after a user stops typing. HTMX can do this with HTML attributes, without a separate script. The home route returns this HTML:
<input
type="search"
name="search"
placeholder="Search"
hx-post="/search"
hx-trigger="input changed delay:500ms, search"
hx-target="#search-results"
/>
<table class="table">
<thead>
<tr>
<th>Title</th>
<th>Author</th>
<th>ISBN</th>
</tr>
</thead>
<tbody id="search-results"></tbody>
</table>hx-post sends a POST request to /search either 500ms after the input changes or when the input is submitted. The response replaces the tbody with the ID search-results.
Any server framework can handle this. I used FastAPI, with routes for GET / and POST /search:
app = FastAPI()
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
return """<html>
<head>
<script src="./htmx.js">
</head>
<body>
<input type="search"
name="search" placeholder="Search"
hx-post="/search"
hx-trigger="input changed delay:500ms, search"
hx-target="#search-results">
<table class="table">
<thead>
<tr>
<th>Title</th>
<th>Author</th>
<th>ISBN</th>
</tr>
</thead>
<tbody id="search-results">
</tbody>
</table>
</body>
</html>
"""
@app.post("/search", response_class=HTMLResponse)
async def search(request: Request, db = Depends(get_database)):
body = await request.body()
searched = db.search_books(body)
response = ""
for book in searched:
response += (f"<tr><td>{book.title}</td>"
f"<td>{book.author}</td>"
f"<td>{book.isbn}</td></tr>")
return responseFor a larger project, I would move the HTML into a template and render it with Jinja.
Writing HTML in Kotlin
HTML templates can get long and hard to manage as a service grows.
Libraries such as Thymeleaf on the JVM and jinja2-fragments in Python let a server render part of a template, which works well with HTMX.
Kotlin also has an experimental HTML library5 that lets you build pages with Kotlin code. It uses a domain-specific language (DSL): functions arranged to describe HTML.
Kotlin's extension functions and lambdas make this possible:
// extension function for String class
fun String.transform(transform: (String) -> String): String {
return transform(this)
}
// calling extension function with lambda
fun main() {
val string = "Hello, World!"
// call lambda directly if it is the last argument of the function
val transformed = string.transform { s ->
// format string
"$s Hello, World again!"
}
println(transformed)
}These features let us build HTML from functions that can be combined and reused. Together with routing in Ktor6, my recent favorite web framework, they let us write the server like this. I find this easier to manage than templates in a static directory or HTML inside formatted strings.
fun FlowContent.index() = body {
input {
type = "search"
name = "search"
placeholder = "Search"
attributes["hx-post"] = "/search"
attributes["hx-trigger"] = "input changed delay:500ms, search"
attributes["hx-target"] = "#search-results"
}
table {
classes = setOf("table")
thead {
tr {
th { +"Title" }
th { +"Author" }
th { +"ISBN" }
}
}
tbody {
id = "search-results"
}
}
}
data class Book(
val title: String,
val author: String,
val isbn: String
)
fun FlowContent.tableRow(book: Book) = tr {
td { +book.title }
td { +book.author }
td { +book.isbn }
}The main module then needs only this:
fun Application.module() {
routing {
get("/") {
call.responseHtml(HttpStatusCode.OK) {
index()
}
}
post("/search") {
val body = call.receiveText()
val books: List<Book> = database.searchBooks(body)
call.responseHtml(HttpStatusCode.OK) {
books.map { book ->
tableRow(book)
}
}
}
}
}I plan to write about setting up Ktor, HTMX, and Tailwind CSS in another post, possibly with component libraries that leave the styling to the app.