{"id":448,"date":"2025-03-29T17:25:53","date_gmt":"2025-03-29T17:25:53","guid":{"rendered":"https:\/\/imcodinggenius.com\/?p=448"},"modified":"2025-03-29T17:25:53","modified_gmt":"2025-03-29T17:25:53","slug":"a-quick-guide-to-htmx-in-kotlin","status":"publish","type":"post","link":"https:\/\/imcodinggenius.com\/?p=448","title":{"rendered":"A Quick Guide to htmx in Kotlin"},"content":{"rendered":"<p>To be more specific, we will see a step-by-step process of writing <strong>HTML <\/strong>with typesafe <strong>Kotlin DSL<\/strong>, integrating <strong>htmx<\/strong>, and handling requests with <strong>Ktor<\/strong>. <\/p>\n<p>Eventually, we will get the below user table:<\/p>\n\n<p>But before we start, just a short disclaimer: although the main focus for this tutorial is the htmx \/ Kotlin \/ Ktor combination, I decided to bring <strong>Tailwind CSS<\/strong> to the project with the help of <a href=\"https:\/\/www.material-tailwind.com\/\" target=\"_blank\" rel=\"noopener\">Material Tailwind components<\/a>. This way, I wanted to showcase the code that is closer to real-life scenarios. Not a next, plain HTML example. <\/p>\n<p>So, although I tried my best, please keep in mind that the HTML part may need some more love when adapting <\/p>\n<p>Note: if you enjoy this content and would like to learn Ktor step-by-step, then check out my <a href=\"https:\/\/codersee.com\/courses\/ktor-server-pro\/\">Ktor Server Pro course<\/a>.<\/p>\n<h2 class=\"wp-block-heading\">What is htmx? <\/h2>\n<p>If you are here, then there is a high chance you\u2019ve been looking for a Kotlin &amp; htmx combination, so you already know what it is. <\/p>\n<p>Nevertheless, so we are all on on the same page: <\/p>\n<p>It is a library that allows you to access modern browser features directly from HTML, rather than using javascript.<\/p>\n<p>And we could add plenty of other things here, like the fact that it is small, dependency-free, and allows us to use AJAX, CSS Transitions, WebSockets, and so on. <\/p>\n<p>But, IMO, the most important thing from the practical standpoint is that we can use attributes in HTML, and the library will do the \u201cmagic\u201d for us:<\/p>\n<p>&lt;td&gt;<br \/>\n  &lt;button hx-delete=&#187;\/users\/7a9079f0-c5a2-45d0-b4ae-e304b6908787&#8243; hx-swap=&#187;outerHTML&#187; hx-target=&#187;closest tr&#187;&gt;<\/p>\n<p>&#8230; the rest<\/p>\n<p>The above following snippet means that when we click the button:<\/p>\n<p>the DELETE \/users\/7a9079f0-c5a2-45d0-b4ae-e304b6908787 request is made<\/p>\n<p>the closest tr element is replaced with the HTML response we receive<\/p>\n<p>And I believe this is all we need to know for now. <\/p>\n<p>Again, a short note from my end: the <a href=\"https:\/\/htmx.org\/docs\/\" target=\"_blank\" rel=\"noopener\">htmx documentation<\/a> is a great resource, and I will refer a lot to it throughout this course to not reinvent the wheel. But at the same time, I want to deliver you a fully-contained article so that you don\u2019t need to jump between pages <\/p>\n<h2 class=\"wp-block-heading\">Generate Ktor Project<\/h2>\n<p>As the first step, let\u2019s quickly generate a new Ktor project using <a href=\"https:\/\/start.ktor.io\/\">https:\/\/start.ktor.io\/<\/a>:<\/p>\n<p>As we can see, the only plugin we need to select is the <strong>Kotlin HTML DSL <\/strong>(this way, the Routing plugin is added, too).<\/p>\n<p>Regarding the config, we are going to use <strong>Ktor 3.1.1<\/strong> with <strong>Netty<\/strong>, <strong>YAML <\/strong>config, and <strong>without a version catalog<\/strong>. But, of course, feel free to adjust it here according to your needs.<\/p>\n<p>With that done, let\u2019s download the project and import it to our IDE. <\/p>\n<h2 class=\"wp-block-heading\">Create User Repository<\/h2>\n<p>Following, let\u2019s add the repository package and introduce a simple, in-memory user repository:<\/p>\n<p>data class User(<br \/>\n    val id: String = UUID.randomUUID().toString(),<br \/>\n    val firstName: String,<br \/>\n    val lastName: String,<br \/>\n    val enabled: Boolean,<br \/>\n    val createdAt: LocalDateTime = LocalDateTime.now(),<br \/>\n)<\/p>\n<p>class UserRepository {<\/p>\n<p>    private val users = mutableListOf(<br \/>\n        User(firstName = &#171;Jane&#187;, lastName = &#171;Doe&#187;, enabled = true),<br \/>\n        User(firstName = &#171;John&#187;, lastName = &#171;Smith&#187;, enabled = true),<br \/>\n        User(firstName = &#171;Alice&#187;, lastName = &#171;Johnson&#187;, enabled = false),<br \/>\n        User(firstName = &#171;Bob&#187;, lastName = &#171;Williams&#187;, enabled = true),<br \/>\n    )<\/p>\n<p>    fun create(firstName: String, lastName: String, enabled: Boolean): User =<br \/>\n        User(firstName = firstName, lastName = lastName, enabled = enabled)<br \/>\n            .also(users::add)<\/p>\n<p>    fun findAll(): List&lt;User&gt; = users<\/p>\n<p>    fun delete(id: String): Boolean = users.removeIf { it.id == id }<br \/>\n}<\/p>\n<p>As we can see, nothing spectacular. Just 3 functions responsible for creating, searching, and deleting users. <\/p>\n<h2 class=\"wp-block-heading\">Return HTML Response in Ktor<\/h2>\n<p>Following, let\u2019s add the routing package and Routing.kt: <\/p>\n<p>fun Application.configureRouting(userRepository: UserRepository) {<br \/>\n    routing {<\/p>\n<p>        get(&#171;\/&#187;) {<br \/>\n            call.respondHtml {<br \/>\n                renderIndex(userRepository)<br \/>\n            }<br \/>\n        }<br \/>\n    }<\/p>\n<p>}<\/p>\n<p>And update the main Application.kt to incorporate those changes: <\/p>\n<p>fun Application.module() {<br \/>\n    val userRepository = UserRepository()<\/p>\n<p>    configureRouting(userRepository)<br \/>\n}<\/p>\n<p>In a moment, we will add the renderIndex function, but for now, let\u2019s focus on the above. <\/p>\n<p>Long story short, the above code instructs the Ktor server to respond with the HTML response whenever it reaches the root path. By default, the localhost:8080. <\/p>\n<h2 class=\"wp-block-heading\">Generate HTML page with Kotlin DSL<\/h2>\n<p>With that done, we have everything we need to start returning HTML responses. So in this section, we will prepare the baseline for htmx. <\/p>\n<p>Note: if you feel that continuous server restarting is painful, please check out <a href=\"https:\/\/codersee.com\/ktor-auto-reload\/\">how to enable auto-reload in Ktor?<\/a>  <\/p>\n<h3 class=\"wp-block-heading\">Render Homepage<\/h3>\n<p>As the first step, let\u2019s add the html package and the renderIndex function in Index.kt: <\/p>\n<p>import com.codersee.repository.UserRepository<br \/>\nimport kotlinx.html.*<\/p>\n<p>fun HTML.renderIndex(userRepository: UserRepository) {<br \/>\n    body {<br \/>\n        div {<br \/>\n            insertHeader()<br \/>\n        }<\/p>\n<p>    }<br \/>\n}<\/p>\n<p>private fun FlowContent.insertHeader() {<br \/>\n    h5 {<br \/>\n        +&#187;Users list&#187;<br \/>\n    }<br \/>\n}<\/p>\n<p>At this point, such a structure is overengineering. Nevertheless, our codebase is about to grow quickly in this tutorial, so we rely on Kotlin extension functions from the very beginning. <\/p>\n<p>And as we can see, we use the HTML that represents the root element of an HTML document. Inside it, we can define the structure in a type-safe manner, thanks to the <a href=\"https:\/\/codersee.com\/kotlin-type-safe-builders-make-your-custom-dsl\/\">Kotlin DSL<\/a> feature. For the inner tags, we can use the FlowContent that is the marker interface for plenty of classes representing HTML tags, like div, headers, or the body.<\/p>\n<p>And before we rerun the application, we must add the necessary import in Routing.kt: <\/p>\n<p>import com.codersee.repository.html.renderIndex<\/p>\n<p>With that done, let\u2019s rerun the application and verify that everything is working. <\/p>\n<h3 class=\"wp-block-heading\">Insert HTML Form<\/h3>\n<p>Following, let\u2019s use the HTML DSL to define the user form. <\/p>\n<p>As the first step, let\u2019s add the Form.kt to inside the html package:<\/p>\n<p>import kotlinx.html.*<\/p>\n<p>fun FlowContent.insertUserForm() {<br \/>\n    div {<br \/>\n        form {<br \/>\n            div {<br \/>\n                div {<br \/>\n                    label {<br \/>\n                        htmlFor = &#171;first-name&#187;<br \/>\n                        +&#187;First Name&#187;<br \/>\n                    }<br \/>\n                    input {<br \/>\n                        type = InputType.text<br \/>\n                        name = &#171;first-name&#187;<br \/>\n                        id = &#171;first-name&#187;<br \/>\n                        placeholder = &#171;First Name&#187;<br \/>\n                    }<br \/>\n                }<br \/>\n                div {<br \/>\n                    label {<br \/>\n                        htmlFor = &#171;last-name&#187;<br \/>\n                        +&#187;Last Name&#187;<br \/>\n                    }<br \/>\n                    input {<br \/>\n                        type = InputType.text<br \/>\n                        name = &#171;last-name&#187;<br \/>\n                        id = &#171;last-name&#187;<br \/>\n                        placeholder = &#171;Last Name&#187;<br \/>\n                    }<br \/>\n                }<br \/>\n                div {<br \/>\n                    label {<br \/>\n                        +&#187;Account enabled&#187;<br \/>\n                    }<br \/>\n                    div {<br \/>\n                        div {<br \/>\n                            input {<br \/>\n                                type = InputType.radio<br \/>\n                                name = &#171;enabled-radio&#187;<br \/>\n                                id = &#171;radio-button-1&#187;<br \/>\n                                value = &#171;true&#187;<br \/>\n                            }<br \/>\n                            label {<br \/>\n                                htmlFor = &#171;radio-button-1&#187;<br \/>\n                                +&#187;Yes&#187;<br \/>\n                            }<br \/>\n                        }<br \/>\n                        div {<br \/>\n                            input {<br \/>\n                                type = InputType.radio<br \/>\n                                name = &#171;enabled-radio&#187;<br \/>\n                                id = &#171;radio-button-2&#187;<br \/>\n                                value = &#171;false&#187;<br \/>\n                                checked = true<br \/>\n                            }<br \/>\n                            label {<br \/>\n                                htmlFor = &#171;radio-button-2&#187;<br \/>\n                                +&#187;No&#187;<br \/>\n                            }<br \/>\n                        }<br \/>\n                    }<br \/>\n                }<br \/>\n                div {<br \/>\n                    button {<br \/>\n                        +&#187;Add user&#187;<br \/>\n                    }<br \/>\n                }<br \/>\n            }<br \/>\n        }<br \/>\n    }<br \/>\n}<\/p>\n<p>As we can see, the <strong>Kotlin DSL allows us to define everything in a neat, structured manner<\/strong>. And although I had a chance to use it in various places, with HTML, it feels so\u2026natural. We write the code pretty similar to HTML.<\/p>\n<p>Of course, before heading to the next part, let\u2019s make use of our function: <\/p>\n<p>fun HTML.renderIndex(userRepository: UserRepository) {<br \/>\n    body {<br \/>\n        div {<br \/>\n            insertHeader()<br \/>\n            insertUserForm()<br \/>\n        }<\/p>\n<p>    }<br \/>\n}<\/p>\n<p>We can clearly see that the extraction was a good idea  <\/p>\n<h3 class=\"wp-block-heading\">Create User Table<\/h3>\n<p>As the next step, let\u2019s add the UserTable.kt: <\/p>\n<p>import com.codersee.repository.User<br \/>\nimport kotlinx.html.*<br \/>\nimport java.time.format.DateTimeFormatter<\/p>\n<p>fun FlowContent.insertUserTable(users: List&lt;User&gt;) {<br \/>\n    div {<br \/>\n        table {<br \/>\n            thead {<br \/>\n                tr {<br \/>\n                    th {<br \/>\n                        +&#187;User&#187;<br \/>\n                    }<br \/>\n                    th {<br \/>\n                        +&#187;Status&#187;<br \/>\n                    }<br \/>\n                    th {<br \/>\n                        +&#187;Created At&#187;<br \/>\n                    }<br \/>\n                    th {}<br \/>\n                }<br \/>\n            }<\/p>\n<p>            tbody {<br \/>\n                id = &#171;users-table&#187;<\/p>\n<p>                users.forEach { user -&gt;<br \/>\n                    tr {<br \/>\n                        insertUserRowCells(user)<br \/>\n                    }<br \/>\n                }<br \/>\n            }<br \/>\n        }<br \/>\n    }<br \/>\n}<\/p>\n<p>fun TR.insertUserRowCells(user: User) {<br \/>\n    td {<br \/>\n        div {<br \/>\n            p {<br \/>\n                +&#187;${user.firstName} ${user.lastName}&#187;<br \/>\n            }<br \/>\n        }<br \/>\n    }<br \/>\n    td {<br \/>\n        div {<br \/>\n            div {<br \/>\n                val enabledLabel = if (user.enabled) &#171;Enabled&#187; else &#171;Disabled&#187;<br \/>\n                val labelColor = if (user.enabled) &#171;green&#187; else &#171;red&#187;<\/p>\n<p>                span { +enabledLabel }<br \/>\n            }<br \/>\n        }<br \/>\n    }<br \/>\n    td {<br \/>\n        p {<br \/>\n            +user.createdAt.format(DateTimeFormatter.ofPattern(&#171;yyyy-MM-dd HH:mm:ss&#187;))<br \/>\n        }<br \/>\n    }<br \/>\n    td {<br \/>\n        button {<br \/>\n            + &#171;Delete&#187;<\/p>\n<p>        }<br \/>\n    }<br \/>\n}<\/p>\n<p>As we can see this time, the Kotlin HTML DSL allows us to easily generate <strong>dynamic HTML tags<\/strong>. We use the passed user list to create a row for every user we \u201cpersisted\u201d. We even make the decision about the label and future color based on the list. <\/p>\n<p>Again, let\u2019s get back to Routing.kt and invoke our function:<\/p>\n<p>fun HTML.renderIndex(userRepository: UserRepository) {<br \/>\n    body {<br \/>\n        div {<br \/>\n            insertHeader()<br \/>\n            insertUserForm()<br \/>\n            insertUserTable(userRepository.findAll())<br \/>\n        }<\/p>\n<p>    }<br \/>\n}<\/p>\n<p>And although I am pretty sure it won\u2019t become the eighth wonder of the World, our application starts looking similar to what we want to achieve:<\/p>\n<h2 class=\"wp-block-heading\">htmx and Kotlin<\/h2>\n<p>With all of that done, we have everything prepared to start actually working with <strong>htmx and Kotlin<\/strong>.<\/p>\n<p>Again, you will see a lot of references taken from the <a href=\"https:\/\/htmx.org\/docs\/\">docs<\/a> (which I encourage you to visit after this tutorial).<\/p>\n<h3 class=\"wp-block-heading\"> Import<\/h3>\n<p>As the first step, let\u2019s import <strong>htmx<\/strong>. <\/p>\n<p>Again, it is nothing else than the JavaScript library, so the only thing we need is to add it inside the script block of our HTML. <\/p>\n<p>And the easiest way is to simply fetch it from their CDN and put it inside the Kotlin script DSL block:<\/p>\n<p>fun HTML.renderIndex(userRepository: UserRepository) {<br \/>\n    head {<br \/>\n        script {<br \/>\n            src = &#171;https:\/\/unpkg.com\/htmx.org@2.0.4&#187;<br \/>\n        }<br \/>\n    }<br \/>\n    body {<br \/>\n        div {<br \/>\n            insertHeader()<br \/>\n            insertUserForm()<br \/>\n            insertUserTable(userRepository.findAll())<br \/>\n        }<\/p>\n<p>    }<br \/>\n}<\/p>\n<h3 class=\"wp-block-heading\">AJAX Requests \u2013 Create User<\/h3>\n<p>As we saw in the very beginning, htmx allows us to define requests with attributes. <\/p>\n<p>To be more specific, we can use the following attributes: <\/p>\n<p>hx-get<\/p>\n<p>hx-post<\/p>\n<p>hx-put<\/p>\n<p>hx-patch<\/p>\n<p>hx-delete<\/p>\n<p>And, long story short, when the element is triggered, an AJAX request is made to the specified URL.<\/p>\n<p>So, let\u2019s update our form then: <\/p>\n<p>fun FlowContent.insertUserForm() {<br \/>\n    div {<br \/>\n        form {<br \/>\n            attributes[&#171;hx-post&#187;] = &#171;\/users&#187;<\/p>\n<p>&#8230; the rest<\/p>\n<p>This way, our html now contains:<\/p>\n<p>&lt;form hx-post=&#187;\/users&#187;&gt;<\/p>\n<p>And when we hit the Add user button, we can see that the request is triggered (but, it results in 404 response given we have no handler). <\/p>\n<p>So, let\u2019s add the endpoint responsible for user creation: <\/p>\n<p>fun Application.configureRouting(userRepository: UserRepository) {<br \/>\n    routing {<\/p>\n<p>        get(&#171;\/&#187;) {<br \/>\n            call.respondHtml {<br \/>\n                renderIndex(userRepository)<br \/>\n            }<br \/>\n        }<\/p>\n<p>        route(&#171;\/users&#187;) {<\/p>\n<p>            post {<br \/>\n                val formParams = call.receiveParameters()<br \/>\n                val firstName = formParams[&#171;first-name&#187;]!!<br \/>\n                val lastName = formParams[&#171;last-name&#187;]!!<br \/>\n                val enabled = formParams[&#171;enabled-radio&#187;]!!.toBoolean()<\/p>\n<p>                val createdItem = userRepository.create(firstName, lastName, enabled)<\/p>\n<p>                val todoItemHtml = createHTML().tr { insertUserRowCells(createdItem) }<\/p>\n<p>                call.respondText(<br \/>\n                    todoItemHtml,<br \/>\n                    contentType = ContentType.Text.Html,<br \/>\n                )<br \/>\n            }<br \/>\n        }<br \/>\n    }<\/p>\n<p>}<\/p>\n<p>At this point, it should not be a surprise, but we can see that in Ktor, we can do that quite easily. <\/p>\n<p>Our code snippet will read the form parameters sent from the browser, \u201ccreate\u201d a new user, and return a 200 OK response with:<\/p>\n<p>&lt;tr&gt;<br \/>\n  &lt;td&gt;<br \/>\n    &lt;div&gt;<br \/>\n      &lt;p&gt;Admiral Jahas&lt;\/p&gt;<br \/>\n    &lt;\/div&gt;<br \/>\n  &lt;\/td&gt;<br \/>\n  &lt;td&gt;<br \/>\n    &lt;div&gt;<br \/>\n      &lt;div&gt;&lt;span&gt;Disabled&lt;\/span&gt;&lt;\/div&gt;<br \/>\n    &lt;\/div&gt;<br \/>\n  &lt;\/td&gt;<br \/>\n  &lt;td&gt;<br \/>\n    &lt;p&gt;2025-03-29 08:16:40&lt;\/p&gt;<br \/>\n  &lt;\/td&gt;<br \/>\n  &lt;td&gt;&lt;button&gt;Delete&lt;\/button&gt;&lt;\/td&gt;<br \/>\n&lt;\/tr&gt;<\/p>\n<p>The <strong>important <\/strong>thing to mention here is that respondHtml requires us to respond with whole body! So, to bypass that, we use the respondText function and set the content type as HTML. <\/p>\n<p>However, when we open up the browser, we can see this:<\/p>\n<p>And I am pretty sure that is not what we wanted  <\/p>\n<h3 class=\"wp-block-heading\">htmx Target<\/h3>\n<p><strong>Lesson one:<\/strong> if we want to instruct htmx to load the response into a different element than the one that made the request, we must use the hx-target attribute that takes the CSS selector, or:<\/p>\n<p>this keyword- to refer to the element with hx-target attribute<\/p>\n<p>closest, next, previous &lt;CSS selector&gt; (like closest div)- to target the closest ancestor element or itself<\/p>\n<p>find &lt;CSS selector \u2013 to target the first child descendant element that matches the given CSS selector<\/p>\n<p>As a proof, let\u2019s take a look at what happened previously: <\/p>\n<p>As we can see, the table row was inserted inside the form. And that does not make sense, at all. <\/p>\n<p>So, to fix that, let\u2019s target the table tbody instead: <\/p>\n<p>fun FlowContent.insertUserForm() {<br \/>\n    div {<br \/>\n        form {<br \/>\n            attributes[&#171;hx-post&#187;] = &#171;\/users&#187;<br \/>\n            attributes[&#171;hx-target&#187;] = &#171;#users-table&#187;<\/p>\n<p>As a result, all the other rows are deleted, but it seems to be closer to what we want:<\/p>\n<h3 class=\"wp-block-heading\">Swapping in htmx<\/h3>\n<p><strong>Next lesson:<\/strong> by default, <strong>htmx replaces the innerHTML of the target element<\/strong>. <\/p>\n<p>So, in our case, the user was added successfully. We can even refresh the page and see that the array contains all created users. However, we have not defined the hx-swap so the tbody inner HTML was deleted, and our returned one was inserted instead. <\/p>\n<p>So, we must add the hx-swap with one of the following values:<\/p>\n<p>innerHTML\u2013 puts the content inside the target element<\/p>\n<p>outerHTML\u2013 replaces the entire target element with the returned content<\/p>\n<p>afterbegin\u2013 prepends the content before the first child inside the target<\/p>\n<p>beforebegin\u2013 prepends the content before the target in the target\u2019s parent element<\/p>\n<p>beforeend\u2013 appends the content after the last child inside the target<\/p>\n<p>afterend\u2013 appends the content after the target in the target\u2019s parent element<\/p>\n<p>delete\u2013 deletes the target element regardless of the response<\/p>\n<p>none\u2013 does not append content from response (Out of Band Swaps and Response Headers will still be processed)<\/p>\n<p>And in our case, the beforeend is the one we should pick to append the created user at the end of the list:<\/p>\n<p>fun FlowContent.insertUserForm() {<br \/>\n    div {<br \/>\n        form {<br \/>\n            attributes[&#171;hx-post&#187;] = &#171;\/users&#187;<br \/>\n            attributes[&#171;hx-target&#187;] = &#171;#users-table&#187;<br \/>\n            attributes[&#171;hx-swap&#187;] = &#171;beforeend&#187;<\/p>\n<p>When we restart the app, everything works fine!  <\/p>\n<h3 class=\"wp-block-heading\">Dynamic htmx Tags in Kotlin<\/h3>\n<p>At this point, we know how to display and add new users with htmx. So, let\u2019s learn how to delete them.<\/p>\n<p>As the first step, let\u2019s prepare a Ktor handler inside the route(&#171;\/users&#187;) for the DELETE request:<\/p>\n<p>delete(&#171;\/{id}&#187;) {<br \/>\n    val id = call.parameters[&#171;id&#187;]!!<\/p>\n<p>    userRepository.delete(id)<\/p>\n<p>    call.respond(HttpStatusCode.OK)<br \/>\n}<\/p>\n<p>With that code, whenever a DELETE \/users\/{some-id} is made, we remove the user from our list and return 200 OK.<\/p>\n<p><strong>Important lesson here:<\/strong> for simplicity, we return 200 OK (and not 204 No Content), because by default, htmx ignores successful responses other than 200.<\/p>\n<p>Following, let\u2019s update our button: <\/p>\n<p>td {<br \/>\n        button {<br \/>\n            attributes[&#171;hx-delete&#187;] = &#171;\/users\/${user.id}&#187;<br \/>\n            attributes[&#171;hx-swap&#187;] = &#171;outerHTML&#187;<br \/>\n            attributes[&#171;hx-target&#187;] = &#171;closest tr&#187;<\/p>\n<p>So, firstly, whenever we generate our button, we use <strong>Kotlin string interpolation<\/strong> to put the user identifier in the hx-delete attribute value. A neat and easy way to achieve that with Kotlin. <\/p>\n<p>When it comes to swapping, we want to find the closest tr parent and swap the entire element with the response. And as the response contains <strong>nothing<\/strong>, it will be simply removed<\/p>\n<p>After we rerun the application, we will see everything working perfectly fine!<\/p>\n<h3 class=\"wp-block-heading\">Error Handling with Ktor and htmx<\/h3>\n<p>Following, let\u2019s learn how we can handle any Ktor error response in htmx. <\/p>\n<p>For that purpose, let\u2019s update the POST handler in Ktor:<\/p>\n<p>post {<br \/>\n    val formParams = call.receiveParameters()<br \/>\n    val firstName = formParams[&#171;first-name&#187;]!!<br \/>\n    val lastName = formParams[&#171;last-name&#187;]!!<br \/>\n    val enabled = formParams[&#171;enabled-radio&#187;]!!.toBoolean()<\/p>\n<p>    if (firstName.isBlank() || lastName.isBlank())<br \/>\n        return@post call.respond(HttpStatusCode.BadRequest)<\/p>\n<p>&#8230; the rest of the code<\/p>\n<p>With that validation, whenever first-name or last-name form parameter is blank, the API client receives 400 Bad Request.<\/p>\n<p>After we restart the server and try to make a request without passing first or last name, we see that nothing is happening. No pop-ups, alerts, nothing. The only indication that the request is actually made is thenetwork tab of our browser.<\/p>\n<p>Well, unfortunately (or fortunately?), htmx does not provide any handling out-of-the-box.<\/p>\n<p>But, it throws two events:<\/p>\n<p>htmx:responseError\u2013 in the event of an error response from the server, like 400 Bad Request<\/p>\n<p>htmx:sendError\u2013 in case of connection error<\/p>\n<p>So, let\u2019s add a tiny bit of JS in Kotlin, then: <\/p>\n<p>private fun BODY.insertErrorHandlingScripts() {<br \/>\n    script {<br \/>\n        +&#187;&#187;&#187;<br \/>\n            document.body.addEventListener(&#8216;htmx:responseError&#8217;, function(evt) {<br \/>\n              alert(&#8216;An error occurred! HTTP status:&#8217; + evt.detail.xhr.status);<br \/>\n            });<\/p>\n<p>            document.body.addEventListener(&#8216;htmx:sendError&#8217;, function(evt) {<br \/>\n              alert(&#8216;Server unavailable!&#8217;);<br \/>\n            });<br \/>\n        &#171;&#187;&#187;.trimIndent()<br \/>\n    }<br \/>\n}<\/p>\n<p>And let\u2019s add this script at the end of the body when rendering the homepage:<\/p>\n<p>fun HTML.renderIndex(userRepository: UserRepository) {<br \/>\n    head {<br \/>\n        script {<br \/>\n            src = &#171;https:\/\/unpkg.com\/htmx.org@2.0.4&#187;<br \/>\n        }<br \/>\n    }<br \/>\n    body {<br \/>\n        div {<br \/>\n            insertHeader()<br \/>\n            insertUserForm()<br \/>\n            insertUserTable(userRepository.findAll())<br \/>\n        }<\/p>\n<p>        insertErrorHandlingScripts()<br \/>\n    }<br \/>\n}<\/p>\n<p>Excellent! From now on, whenever the API client receives an error response, the alert is displayed. Moreover, if we turn off our server, we will see the error response, too.<\/p>\n<p>And basically, that is all for the htmx part with Kotlin. From now on, we are going to work on the styling of our application<\/p>\n<h2 class=\"wp-block-heading\">Returning Images in Ktor<\/h2>\n<p>Before we head to the Tailwind CSS part, let\u2019s learn one more thing in Ktor: static responses handling.<\/p>\n<p>So, let\u2019s put the below image in the resources -&gt; img directory:<\/p>\n<p>And let\u2019s add this image as a placeholder to each row in our table: <\/p>\n<p>fun TR.insertUserRowCells(user: User) {<br \/>\n    td {<br \/>\n        div {<br \/>\n            img {<br \/>\n                src = &#171;\/img\/placeholder.png&#187;<br \/>\n            }<\/p>\n<p>When we rerun the application, we can see that it does not work.<\/p>\n<p>Well, to fix that, we must instruct Ktor to serve our resources as static content:<\/p>\n<p>fun Application.configureRouting(userRepository: UserRepository) {<br \/>\n    routing {<br \/>\n        staticResources(&#171;\/img&#187;, &#171;img&#187;)<\/p>\n<p>This time, when we restart the application, we see that placeholders are working fine. <\/p>\n<p>And we will style them in a moment  <\/p>\n<h2 class=\"wp-block-heading\">Styling With Tailwind CSS<\/h2>\n<p>At this point, we have a fully working Kotlin and htmx integration.<\/p>\n<p>So, if we already did something else than the JSON response, let\u2019s make it nice<\/p>\n<h3 class=\"wp-block-heading\">Import Tailwind<\/h3>\n<p>Just like with htmx, let\u2019s use the CDN to import Tailwind to the project:<\/p>\n<p>fun HTML.renderIndex(userRepository: UserRepository) {<br \/>\n    head {<br \/>\n        script {<br \/>\n            src = &#171;https:\/\/unpkg.com\/htmx.org@2.0.4&#187;<br \/>\n        }<br \/>\n        script {<br \/>\n            src = &#171;https:\/\/unpkg.com\/@tailwindcss\/browser@4&#187;<br \/>\n        }<br \/>\n    }<\/p>\n<h3 class=\"wp-block-heading\">Update Index <\/h3>\n<p>Then, let\u2019s navigate to the Index.kt and add adjustments:<\/p>\n<p>fun HTML.renderIndex(userRepository: UserRepository) {<br \/>\n    head {<br \/>\n        script {<br \/>\n            src = &#171;https:\/\/unpkg.com\/htmx.org@2.0.4&#187;<br \/>\n        }<br \/>\n        script {<br \/>\n            src = &#171;https:\/\/unpkg.com\/@tailwindcss\/browser@4&#187;<br \/>\n        }<br \/>\n    }<br \/>\n    body {<br \/>\n        div {<br \/>\n            classes = setOf(&#171;m-auto max-w-5xl w-full overflow-hidden&#187;)<\/p>\n<p>            insertHeader()<br \/>\n            insertUserForm()<br \/>\n            insertUserTable(userRepository.findAll())<br \/>\n        }<\/p>\n<p>        insertErrorHandlingScripts()<br \/>\n    }<br \/>\n}<\/p>\n<p>private fun FlowContent.insertHeader() {<br \/>\n    h5 {<br \/>\n        classes =<br \/>\n            setOf(&#171;py-8 block font-sans text-xl antialiased font-semibold leading-snug tracking-normal text-blue-gray-900&#187;)<\/p>\n<p>        +&#187;Users list&#187;<br \/>\n    }<br \/>\n}<\/p>\n<p>private fun BODY.insertErrorHandlingScripts() {<br \/>\n    script {<br \/>\n        +&#187;&#187;&#187;<br \/>\n            document.body.addEventListener(&#8216;htmx:responseError&#8217;, function(evt) {<br \/>\n              alert(&#8216;An error occurred! HTTP status:&#8217; + evt.detail.xhr.status);<br \/>\n            });<\/p>\n<p>            document.body.addEventListener(&#8216;htmx:sendError&#8217;, function(evt) {<br \/>\n              alert(&#8216;Server unavailable!&#8217;);<br \/>\n            });<br \/>\n        &#171;&#187;&#187;.trimIndent()<br \/>\n    }<br \/>\n}<\/p>\n<p>As we can see, we can use the classes in Kotlin HTML DSL to prive classes names as a Set of String values:<\/p>\n<p>classes = setOf(\u201cm-auto max-w-5xl w-full overflow-hidden\u201d)<\/p>\n<p>In my case, I prefer simply copy-pasting those values instead of separating them with colons.<\/p>\n<h2 class=\"wp-block-heading\">Refactor Form<\/h2>\n<p>Then, let\u2019s update the Form.kt: <\/p>\n<p>fun FlowContent.insertUserForm() {<br \/>\n    div {<br \/>\n        classes = setOf(&#171;mx-auto w-full&#187;)<\/p>\n<p>        form {<br \/>\n            attributes[&#171;hx-post&#187;] = &#171;\/users&#187;<br \/>\n            attributes[&#171;hx-target&#187;] = &#171;#users-table&#187;<br \/>\n            attributes[&#171;hx-swap&#187;] = &#171;beforeend&#187;<\/p>\n<p>            div {<br \/>\n                classes = setOf(&#171;-mx-3 flex flex-wrap&#187;)<\/p>\n<p>                div {<br \/>\n                    classes = setOf(&#171;w-full px-3 sm:w-1\/4&#187;)<\/p>\n<p>                    label {<br \/>\n                        classes = setOf(&#171;mb-3 block text-base font-medium text-[#07074D]&#187;)<\/p>\n<p>                        htmlFor = &#171;first-name&#187;<br \/>\n                        +&#187;First Name&#187;<br \/>\n                    }<br \/>\n                    input {<br \/>\n                        classes =<br \/>\n                            setOf(&#171;w-full rounded-md border border-[#e0e0e0] bg-white py-3 px-6 text-base font-medium text-[#6B7280] outline-none focus:border-[#6A64F1] focus:shadow-md&#187;)<\/p>\n<p>                        type = InputType.text<br \/>\n                        name = &#171;first-name&#187;<br \/>\n                        id = &#171;first-name&#187;<br \/>\n                        placeholder = &#171;First Name&#187;<br \/>\n                    }<br \/>\n                }<br \/>\n                div {<br \/>\n                    classes = setOf(&#171;w-full px-3 sm:w-1\/4&#187;)<\/p>\n<p>                    label {<br \/>\n                        classes = setOf(&#171;mb-3 block text-base font-medium text-[#07074D]&#187;)<\/p>\n<p>                        htmlFor = &#171;last-name&#187;<br \/>\n                        +&#187;Last Name&#187;<br \/>\n                    }<br \/>\n                    input {<br \/>\n                        classes =<br \/>\n                            setOf(&#171;w-full rounded-md border border-[#e0e0e0] bg-white py-3 px-6 text-base font-medium text-[#6B7280] outline-none focus:border-[#6A64F1] focus:shadow-md&#187;)<\/p>\n<p>                        type = InputType.text<br \/>\n                        name = &#171;last-name&#187;<br \/>\n                        id = &#171;last-name&#187;<br \/>\n                        placeholder = &#171;Last Name&#187;<br \/>\n                    }<br \/>\n                }<br \/>\n                div {<br \/>\n                    classes = setOf(&#171;w-full px-3 sm:w-1\/4&#187;)<\/p>\n<p>                    label {<br \/>\n                        classes = setOf(&#171;mb-3 block text-base font-medium text-[#07074D]&#187;)<\/p>\n<p>                        +&#187;Account enabled&#187;<br \/>\n                    }<br \/>\n                    div {<br \/>\n                        classes = setOf(&#171;flex items-center space-x-6 pt-3&#187;)<\/p>\n<p>                        div {<br \/>\n                            classes = setOf(&#171;flex items-center&#187;)<\/p>\n<p>                            input {<br \/>\n                                classes = setOf(&#171;h-5 w-5&#187;)<\/p>\n<p>                                type = InputType.radio<br \/>\n                                name = &#171;enabled-radio&#187;<br \/>\n                                id = &#171;radio-button-1&#187;<br \/>\n                                value = &#171;true&#187;<br \/>\n                            }<br \/>\n                            label {<br \/>\n                                classes = setOf(&#171;pl-3 text-base font-medium text-[#07074D]&#187;)<\/p>\n<p>                                htmlFor = &#171;radio-button-1&#187;<br \/>\n                                +&#187;Yes&#187;<br \/>\n                            }<br \/>\n                        }<br \/>\n                        div {<br \/>\n                            classes = setOf(&#171;flex items-center&#187;)<\/p>\n<p>                            input {<br \/>\n                                classes = setOf(&#171;h-5 w-5&#187;)<\/p>\n<p>                                type = InputType.radio<br \/>\n                                name = &#171;enabled-radio&#187;<br \/>\n                                id = &#171;radio-button-2&#187;<br \/>\n                                value = &#171;false&#187;<br \/>\n                                checked = true<br \/>\n                            }<br \/>\n                            label {<br \/>\n                                classes = setOf(&#171;pl-3 text-base font-medium text-[#07074D]&#187;)<\/p>\n<p>                                htmlFor = &#171;radio-button-2&#187;<br \/>\n                                +&#187;No&#187;<br \/>\n                            }<br \/>\n                        }<br \/>\n                    }<br \/>\n                }<br \/>\n                div {<br \/>\n                    classes = setOf(&#171;w-full px-3 sm:w-1\/4 pt-8&#187;)<\/p>\n<p>                    button {<br \/>\n                        classes =<br \/>\n                            setOf(&#171;cursor-pointer rounded-md bg-slate-800 py-3 px-8 text-center text-base font-semibold text-white outline-none&#187;)<\/p>\n<p>                        +&#187;Add user&#187;<br \/>\n                    }<br \/>\n                }<br \/>\n            }<br \/>\n        }<br \/>\n    }<br \/>\n}<\/p>\n<p>Similarly, we don\u2019t change anything in here apart from adding a bunch of classes. A whooooole bunch of classes  <\/p>\n<h2 class=\"wp-block-heading\">Modify User Table<\/h2>\n<p>As the last step, let\u2019 apply changes to our user table: <\/p>\n<p>fun FlowContent.insertUserTable(users: List&lt;User&gt;) {<br \/>\n    div {<br \/>\n        classes = setOf(&#171;px-0 overflow-scroll&#187;)<\/p>\n<p>        table {<br \/>\n            classes = setOf(&#171;w-full mt-4 text-left table-auto min-w-max&#187;)<\/p>\n<p>            thead {<br \/>\n                tr {<br \/>\n                    th {<br \/>\n                        classes = setOf(&#171;p-4 border-y border-blue-gray-100 bg-blue-gray-50\/50&#187;)<br \/>\n                        +&#187;User&#187;<br \/>\n                    }<br \/>\n                    th {<br \/>\n                        classes = setOf(&#171;p-4 border-y border-blue-gray-100 bg-blue-gray-50\/50&#187;)<br \/>\n                        +&#187;Status&#187;<br \/>\n                    }<br \/>\n                    th {<br \/>\n                        classes = setOf(&#171;p-4 border-y border-blue-gray-100 bg-blue-gray-50\/50&#187;)<br \/>\n                        +&#187;Created At&#187;<br \/>\n                    }<br \/>\n                    th {<br \/>\n                        classes = setOf(&#171;p-4 border-y border-blue-gray-100 bg-blue-gray-50\/50&#187;)<br \/>\n                    }<br \/>\n                }<br \/>\n            }<\/p>\n<p>            tbody {<br \/>\n                id = &#171;users-table&#187;<\/p>\n<p>                users.forEach { user -&gt;<br \/>\n                    tr {<br \/>\n                        insertUserRowCells(user)<br \/>\n                    }<br \/>\n                }<br \/>\n            }<br \/>\n        }<br \/>\n    }<br \/>\n}<\/p>\n<p>fun TR.insertUserRowCells(user: User) {<br \/>\n    td {<br \/>\n        classes = setOf(&#171;p-4 border-b border-blue-gray-50&#187;)<\/p>\n<p>        div {<br \/>\n            classes = setOf(&#171;flex items-center gap-3&#187;)<\/p>\n<p>            img {<br \/>\n                classes = setOf(&#171;relative inline-block h-9 w-9 !rounded-full object-cover object-center&#187;)<\/p>\n<p>                src = &#171;\/img\/placeholder.png&#187;<br \/>\n            }<\/p>\n<p>            p {<br \/>\n                classes = setOf(&#171;block font-sans text-sm antialiased font-normal leading-normal text-blue-gray-900&#187;)<\/p>\n<p>                +&#187;${user.firstName} ${user.lastName}&#187;<br \/>\n            }<br \/>\n        }<br \/>\n    }<br \/>\n    td {<br \/>\n        classes = setOf(&#171;p-4 border-b border-blue-gray-50&#187;)<\/p>\n<p>        div {<br \/>\n            classes = setOf(&#171;w-max&#187;)<\/p>\n<p>            div {<br \/>\n                val enabledLabel = if (user.enabled) &#171;Enabled&#187; else &#171;Disabled&#187;<br \/>\n                val labelColor = if (user.enabled) &#171;green&#187; else &#171;red&#187;<\/p>\n<p>                classes =<br \/>\n                    setOf(&#171;relative grid items-center px-2 py-1 font-sans text-xs font-bold text-black-900 uppercase rounded-md select-none whitespace-nowrap bg-$labelColor-500\/20&#187;)<\/p>\n<p>                span { +enabledLabel }<br \/>\n            }<br \/>\n        }<br \/>\n    }<br \/>\n    td {<br \/>\n        classes = setOf(&#171;p-4 border-b border-blue-gray-50&#187;)<\/p>\n<p>        p {<br \/>\n            classes = setOf(&#171;block font-sans text-sm antialiased font-normal leading-normal text-blue-gray-900&#187;)<\/p>\n<p>            +user.createdAt.format(DateTimeFormatter.ofPattern(&#171;yyyy-MM-dd HH:mm:ss&#187;))<br \/>\n        }<br \/>\n    }<br \/>\n    td {<br \/>\n        classes = setOf(&#171;p-4 border-b border-blue-gray-50&#187;)<\/p>\n<p>        button {<br \/>\n            classes =<br \/>\n                setOf(&#171;cursor-pointer relative h-10 max-h-[40px] w-10 max-w-[40px] select-none rounded-lg text-center align-middle font-sans text-xs font-medium uppercase text-gray-900 transition-all hover:bg-gray-900\/10 active:bg-gray-900\/20 disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none&#187;)<br \/>\n            attributes[&#171;hx-delete&#187;] = &#171;\/users\/${user.id}&#187;<br \/>\n            attributes[&#171;hx-swap&#187;] = &#171;outerHTML&#187;<br \/>\n            attributes[&#171;hx-target&#187;] = &#171;closest tr&#187;<\/p>\n<p>            unsafe {<br \/>\n                +&#187;&#187;&#187;<br \/>\n                    &lt;span class=&#187;absolute transform -translate-x-1\/2 -translate-y-1\/2 top-1\/2 left-1\/2&#8243;&gt;<br \/>\n                        &lt;svg xmlns=&#187;http:\/\/www.w3.org\/2000\/svg&#187; fill=&#187;none&#187; viewBox=&#187;0 0 24 24&#8243;<br \/>\n                             stroke-width=&#187;2&#8243; stroke=&#187;currentColor&#187; class=&#187;w-6 h-6&#8243;&gt;<br \/>\n                            &lt;path stroke-linecap=&#187;round&#187; stroke-linejoin=&#187;round&#187;<br \/>\n                                  d=&#187;M6 18L18 6M6 6l12 12&#8243;\/&gt;<br \/>\n                        &lt;\/svg&gt;<br \/>\n                    &lt;\/span&gt;<br \/>\n                &#171;&#187;&#187;.trimIndent()<br \/>\n            }<\/p>\n<p>        }<br \/>\n    }<br \/>\n}<\/p>\n<p>And here, apart from the CSS classes, we also added the X icon with the unsafe function from Kotlin HTML DSL:<\/p>\n<p>unsafe {<br \/>\n    +&#187;&#187;&#187;<br \/>\n        &lt;span class=&#187;absolute transform -translate-x-1\/2 -translate-y-1\/2 top-1\/2 left-1\/2&#8243;&gt;<br \/>\n            &lt;svg xmlns=&#187;http:\/\/www.w3.org\/2000\/svg&#187; fill=&#187;none&#187; viewBox=&#187;0 0 24 24&#8243;<br \/>\n                 stroke-width=&#187;2&#8243; stroke=&#187;currentColor&#187; class=&#187;w-6 h-6&#8243;&gt;<br \/>\n                &lt;path stroke-linecap=&#187;round&#187; stroke-linejoin=&#187;round&#187;<br \/>\n                      d=&#187;M6 18L18 6M6 6l12 12&#8243;\/&gt;<br \/>\n            &lt;\/svg&gt;<br \/>\n        &lt;\/span&gt;<br \/>\n    &#171;&#187;&#187;.trimIndent()<br \/>\n}<\/p>\n<p>And voila! When we run the application now, we should see a pretty decent-looking UI<\/p>\n<h2 class=\"wp-block-heading\">Summary<\/h2>\n<p>That\u2019s all for this tutorial on how to work with htmx and Kotlin HTML DSL in Ktor.<\/p>\n<p>Again, if you are tired of wasting your time looking for good Ktor resources, then check out my <a href=\"https:\/\/codersee.com\/courses\/ktor-server-pro\/\">Ktor Server Pro course<\/a>:<\/p>\n<p> Over 15 hours of video content divided into over 130 lessons<\/p>\n<p> Hands-on approach: together, we implement 4 actual services<\/p>\n<p> top technologies: you will learn not only Ktor, but also how to integrate it with modern stack including JWT, PostgreSQL, MySQL, MongoDB, Redis, and Testcontainers.<\/p>\n<p>Lastly, you can find the source code for this lesson in <a href=\"https:\/\/github.com\/codersee-blog\/ktor-htmx\" target=\"_blank\" rel=\"noopener\">this GitHub repository<\/a>.<\/p>\n<p>The post <a href=\"https:\/\/codersee.com\/quick-quide-to-htmx-kotlin\/\">A Quick Guide to htmx in Kotlin<\/a> appeared first on <a href=\"https:\/\/codersee.com\/\">Codersee &#8212; Kotlin on the backend<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>To be more specific, we will see a step-by-step process of writing HTML with typesafe Kotlin DSL, integrating htmx, and handling requests with Ktor. Eventually, we will get the below user table: But before we start, just a short disclaimer: although the main focus for this tutorial is the htmx &#8230; <\/p>\n<div><a class=\"more-link bs-book_btn\" href=\"https:\/\/imcodinggenius.com\/?p=448\">Read More<\/a><\/div>\n","protected":false},"author":0,"featured_media":449,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-448","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-news"],"_links":{"self":[{"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=\/wp\/v2\/posts\/448"}],"collection":[{"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=448"}],"version-history":[{"count":0,"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=\/wp\/v2\/posts\/448\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=\/wp\/v2\/media\/449"}],"wp:attachment":[{"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=448"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=448"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=448"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}