<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>htmx Archives - Codersee blog- Kotlin on the backend</title>
	<atom:link href="https://blog.codersee.com/tag/htmx/feed/" rel="self" type="application/rss+xml" />
	<link></link>
	<description>Kotlin &#38; Backend Tutorials - Learn Through Practice.</description>
	<lastBuildDate>Thu, 17 Apr 2025 09:45:17 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://blog.codersee.com/wp-content/uploads/2025/04/cropped-codersee_logo_circle_2-32x32.png</url>
	<title>htmx Archives - Codersee blog- Kotlin on the backend</title>
	<link></link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>A Quick Guide to htmx in Kotlin</title>
		<link>https://blog.codersee.com/quick-quide-to-htmx-kotlin/</link>
					<comments>https://blog.codersee.com/quick-quide-to-htmx-kotlin/#respond</comments>
		
		<dc:creator><![CDATA[Piotr]]></dc:creator>
		<pubDate>Sat, 29 Mar 2025 17:25:53 +0000</pubDate>
				<category><![CDATA[Kotlin]]></category>
		<category><![CDATA[Ktor]]></category>
		<category><![CDATA[htmx]]></category>
		<guid isPermaLink="false">https://codersee.com/?p=18012838</guid>

					<description><![CDATA[<p>In this tutorial, we will learn the most important htmx concepts and create a basic application with Kotlin HTML DSL and Tailwind CSS.</p>
<p>The post <a href="https://blog.codersee.com/quick-quide-to-htmx-kotlin/">A Quick Guide to htmx in Kotlin</a> appeared first on <a href="https://blog.codersee.com">Codersee blog- Kotlin on the backend</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<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>



<p>Eventually, we will get the below user table:</p>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1024" height="521" src="http://blog.codersee.com/wp-content/uploads/2025/03/htmx_kotlin_app_screen-1024x521.webp" alt="Image presents a screenshot of result application created in Kotlin and htmx." class="wp-image-18012839" srcset="https://blog.codersee.com/wp-content/uploads/2025/03/htmx_kotlin_app_screen-1024x521.webp 1024w, https://blog.codersee.com/wp-content/uploads/2025/03/htmx_kotlin_app_screen-300x153.webp 300w, https://blog.codersee.com/wp-content/uploads/2025/03/htmx_kotlin_app_screen-768x391.webp 768w, https://blog.codersee.com/wp-content/uploads/2025/03/htmx_kotlin_app_screen.webp 1113w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p></p>



<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="noreferrer noopener nofollow">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>



<p>So, although I tried my best, please keep in mind that the HTML part may need some more love when adapting 🫡</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<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>
</blockquote>



<h2 class="wp-block-heading" id="h-video-tutorial">Video Tutorial</h2>



<p>If you prefer <strong>video content</strong>, then check out my video:</p>


<figure class="wp-block-embed-youtube wp-block-embed is-type-video is-provider-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><a href="https://blog.codersee.com/quick-quide-to-htmx-kotlin/"><img decoding="async" src="https://blog.codersee.com/wp-content/plugins/wp-youtube-lyte/lyteCache.php?origThumbUrl=%2F%2Fi.ytimg.com%2Fvi%2FtstB08EDClw%2Fhqdefault.jpg" alt="YouTube Video"></a><br /><br /><figcaption></figcaption></figure>


<h2 class="wp-block-heading" id="h-what-is-htmx">What is htmx? </h2>



<p>If you are here, then there is a high chance you&#8217;ve been looking for a Kotlin &amp; htmx combination, so you already know what it is. </p>



<p>Nevertheless, so we are all on on the same page: </p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>It is a library that allows you to access modern browser features directly from HTML, rather than using javascript.</p>
</blockquote>



<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>



<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 &#8220;magic&#8221; for us:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="html" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">&lt;td>
  &lt;button hx-delete="/users/7a9079f0-c5a2-45d0-b4ae-e304b6908787" hx-swap="outerHTML" hx-target="closest tr">
... the rest
   </pre>



<p>The above following snippet means that when we click the button:</p>



<ul class="wp-block-list">
<li>the <code>DELETE /users/7a9079f0-c5a2-45d0-b4ae-e304b6908787</code> request is made</li>



<li>the closest tr element is replaced with the HTML response we receive</li>
</ul>



<p>And I believe this is all we need to know for now. </p>



<p>Again, a short note from my end: the <a href="https://htmx.org/docs/" target="_blank" rel="noreferrer 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&#8217;t need to jump between pages 😉</p>



<h2 class="wp-block-heading" id="h-generate-ktor-project">Generate Ktor Project</h2>



<p>As the first step, let&#8217;s quickly generate a new Ktor project using <a href="https://start.ktor.io/">https://start.ktor.io/</a>:</p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="590" src="http://blog.codersee.com/wp-content/uploads/2025/03/ktor_htmx_kotlin_html_dsl-1024x590.png" alt="" class="wp-image-18012840" srcset="https://blog.codersee.com/wp-content/uploads/2025/03/ktor_htmx_kotlin_html_dsl-1024x590.png 1024w, https://blog.codersee.com/wp-content/uploads/2025/03/ktor_htmx_kotlin_html_dsl-300x173.png 300w, https://blog.codersee.com/wp-content/uploads/2025/03/ktor_htmx_kotlin_html_dsl-768x443.png 768w, https://blog.codersee.com/wp-content/uploads/2025/03/ktor_htmx_kotlin_html_dsl.png 1277w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<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>



<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>



<p>With that done, let&#8217;s download the project and import it to our IDE. </p>



<h2 class="wp-block-heading" id="h-create-user-repository">Create User Repository</h2>



<p>Following, let&#8217;s add the <code>repository</code> package and introduce a simple, in-memory user repository:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">data class User(
    val id: String = UUID.randomUUID().toString(),
    val firstName: String,
    val lastName: String,
    val enabled: Boolean,
    val createdAt: LocalDateTime = LocalDateTime.now(),
)
class UserRepository {
    private val users = mutableListOf(
        User(firstName = "Jane", lastName = "Doe", enabled = true),
        User(firstName = "John", lastName = "Smith", enabled = true),
        User(firstName = "Alice", lastName = "Johnson", enabled = false),
        User(firstName = "Bob", lastName = "Williams", enabled = true),
    )
    fun create(firstName: String, lastName: String, enabled: Boolean): User =
        User(firstName = firstName, lastName = lastName, enabled = enabled)
            .also(users::add)
    fun findAll(): List&lt;User> = users
    fun delete(id: String): Boolean = users.removeIf { it.id == id }
}</pre>



<p>As we can see, nothing spectacular. Just 3 functions responsible for creating, searching, and deleting users. </p>



<h2 class="wp-block-heading" id="h-return-html-response-in-ktor">Return HTML Response in Ktor</h2>



<p>Following, let&#8217;s add the <code>routing</code> package and <code>Routing.kt</code>: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">fun Application.configureRouting(userRepository: UserRepository) {
    routing {
        get("/") {
            call.respondHtml {
                renderIndex(userRepository)
            }
        }
    }
}</pre>



<p>And update the main <code>Application.kt</code> to incorporate those changes: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">fun Application.module() {
    val userRepository = UserRepository()
    configureRouting(userRepository)
}</pre>



<p>In a moment, we will add the <code>renderIndex</code> function, but for now, let&#8217;s focus on the above. </p>



<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 <code>localhost:8080</code>. </p>



<h2 class="wp-block-heading" id="h-generate-html-page-with-kotlin-dsl">Generate HTML page with Kotlin DSL</h2>



<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>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Note: if you feel that continuous server restarting is painful, please check out <a href="https://blog.codersee.com/ktor-auto-reload/">how to enable auto-reload in Ktor?</a>  </p>
</blockquote>



<h3 class="wp-block-heading" id="h-render-homepage">Render Homepage</h3>



<p>As the first step, let&#8217;s add the <code>html</code> package and the <code>renderIndex</code> function in <code>Index.kt</code>: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">import com.codersee.repository.UserRepository
import kotlinx.html.*
fun HTML.renderIndex(userRepository: UserRepository) {
    body {
        div {
            insertHeader()
        }
    }
}
private fun FlowContent.insertHeader() {
    h5 {
        +"Users list"
    }
}</pre>



<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>



<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://blog.codersee.com/kotlin-type-safe-builders-make-your-custom-dsl/">Kotlin DSL</a> feature. For the inner tags, we can use the <code>FlowContent</code> that is the marker interface for plenty of classes representing HTML tags, like div, headers, or the body.</p>



<p>And before we rerun the application, we must add the necessary import in <code>Routing.kt</code>: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">import com.codersee.repository.html.renderIndex</pre>



<p>With that done, let&#8217;s rerun the application and verify that everything is working. </p>



<h3 class="wp-block-heading" id="h-insert-html-form">Insert HTML Form</h3>



<p>Following, let&#8217;s use the HTML DSL to define the user form. </p>



<p>As the first step, let&#8217;s add the <code>Form.kt</code> to inside the <code>html</code> package:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">import kotlinx.html.*
fun FlowContent.insertUserForm() {
    div {
        form {
            div {
                div {
                    label {
                        htmlFor = "first-name"
                        +"First Name"
                    }
                    input {
                        type = InputType.text
                        name = "first-name"
                        id = "first-name"
                        placeholder = "First Name"
                    }
                }
                div {
                    label {
                        htmlFor = "last-name"
                        +"Last Name"
                    }
                    input {
                        type = InputType.text
                        name = "last-name"
                        id = "last-name"
                        placeholder = "Last Name"
                    }
                }
                div {
                    label {
                        +"Account enabled"
                    }
                    div {
                        div {
                            input {
                                type = InputType.radio
                                name = "enabled-radio"
                                id = "radio-button-1"
                                value = "true"
                            }
                            label {
                                htmlFor = "radio-button-1"
                                +"Yes"
                            }
                        }
                        div {
                            input {
                                type = InputType.radio
                                name = "enabled-radio"
                                id = "radio-button-2"
                                value = "false"
                                checked = true
                            }
                            label {
                                htmlFor = "radio-button-2"
                                +"No"
                            }
                        }
                    }
                }
                div {
                    button {
                        +"Add user"
                    }
                }
            }
        }
    }
}</pre>



<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&#8230;natural. We write the code pretty similar to HTML.</p>



<p>Of course, before heading to the next part, let&#8217;s make use of our function: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">fun HTML.renderIndex(userRepository: UserRepository) {
    body {
        div {
            insertHeader()
            insertUserForm()
        }
    }
}</pre>



<p>We can clearly see that the extraction was a good idea 😉 </p>



<h3 class="wp-block-heading" id="h-create-user-table">Create User Table</h3>



<p>As the next step, let&#8217;s add the <code>UserTable.kt</code>: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">import com.codersee.repository.User
import kotlinx.html.*
import java.time.format.DateTimeFormatter
fun FlowContent.insertUserTable(users: List&lt;User>) {
    div {
        table {
            thead {
                tr {
                    th {
                        +"User"
                    }
                    th {
                        +"Status"
                    }
                    th {
                        +"Created At"
                    }
                    th {}
                }
            }
            tbody {
                id = "users-table"
                users.forEach { user ->
                    tr {
                        insertUserRowCells(user)
                    }
                }
            }
        }
    }
}
fun TR.insertUserRowCells(user: User) {
    td {
        div {
            p {
                +"${user.firstName} ${user.lastName}"
            }
        }
    }
    td {
        div {
            div {
                val enabledLabel = if (user.enabled) "Enabled" else "Disabled"
                val labelColor = if (user.enabled) "green" else "red"
                span { +enabledLabel }
            }
        }
    }
    td {
        p {
            +user.createdAt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
        }
    }
    td {
        button {
            + "Delete"
        }
    }
}</pre>



<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 &#8220;persisted&#8221;. We even make the decision about the label and future color based on the list. </p>



<p>Again, let&#8217;s get back to <code>Routing.kt</code> and invoke our function:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">fun HTML.renderIndex(userRepository: UserRepository) {
    body {
        div {
            insertHeader()
            insertUserForm()
            insertUserTable(userRepository.findAll())
        }
    }
}
</pre>



<p>And although I am pretty sure it won&#8217;t become the eighth wonder of the World, our application starts looking similar to what we want to achieve:</p>



<figure class="wp-block-image size-full"><img decoding="async" width="382" height="428" src="http://blog.codersee.com/wp-content/uploads/2025/03/ktor_kotlin_html_dsl_plain_application_screen.png" alt="Image is a screenshot of a plain HTML application generated with Kotlin HTML DSL in Ktor" class="wp-image-18512857" style="object-fit:cover" srcset="https://blog.codersee.com/wp-content/uploads/2025/03/ktor_kotlin_html_dsl_plain_application_screen.png 382w, https://blog.codersee.com/wp-content/uploads/2025/03/ktor_kotlin_html_dsl_plain_application_screen-268x300.png 268w" sizes="(max-width: 382px) 100vw, 382px" /></figure>



<h2 class="wp-block-heading" id="h-htmx-and-kotlin">htmx and Kotlin</h2>



<p>With all of that done, we have everything prepared to start actually working with <strong>htmx and Kotlin</strong>.</p>



<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>



<h3 class="wp-block-heading" id="h-import"> Import</h3>



<p>As the first step, let&#8217;s import <strong>htmx</strong>. </p>



<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>



<p>And the easiest way is to simply fetch it from their CDN and put it inside the Kotlin <code>script</code> DSL block:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">fun HTML.renderIndex(userRepository: UserRepository) {
    head {
        script {
            src = "https://unpkg.com/htmx.org@2.0.4"
        }
    }
    body {
        div {
            insertHeader()
            insertUserForm()
            insertUserTable(userRepository.findAll())
        }
    }
}</pre>



<h3 class="wp-block-heading" id="h-ajax-requests-create-user">AJAX Requests &#8211; Create User</h3>



<p>As we saw in the very beginning, htmx allows us to define requests with attributes. </p>



<p>To be more specific, we can use the following attributes: </p>



<ul class="wp-block-list">
<li>hx-get</li>



<li>hx-post</li>



<li>hx-put</li>



<li>hx-patch</li>



<li>hx-delete</li>
</ul>



<p>And, long story short, when the element is triggered, an AJAX request is made to the specified URL.</p>



<p>So, let&#8217;s update our form then: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">fun FlowContent.insertUserForm() {
    div {
        form {
            attributes["hx-post"] = "/users"
... the rest</pre>



<p>This way, our html now contains:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="html" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">&lt;form hx-post="/users"></pre>



<p>And when we hit the <code>Add user</code> button, we can see that the request is triggered (but, it results in <code>404</code> response given we have no handler). </p>



<p>So, let&#8217;s add the endpoint responsible for user creation: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">fun Application.configureRouting(userRepository: UserRepository) {
    routing {
        get("/") {
            call.respondHtml {
                renderIndex(userRepository)
            }
        }
        route("/users") {
            post {
                val formParams = call.receiveParameters()
                val firstName = formParams["first-name"]!!
                val lastName = formParams["last-name"]!!
                val enabled = formParams["enabled-radio"]!!.toBoolean()
                val createdItem = userRepository.create(firstName, lastName, enabled)
                val todoItemHtml = createHTML().tr { insertUserRowCells(createdItem) }
                call.respondText(
                    todoItemHtml,
                    contentType = ContentType.Text.Html,
                )
            }
        }
    }
}</pre>



<p>At this point, it should not be a surprise, but we can see that in Ktor, we can do that quite easily. </p>



<p>Our code snippet will read the form parameters sent from the browser, &#8220;create&#8221; a new user, and return a <code>200 OK</code> response with:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="html" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">&lt;tr>
  &lt;td>
    &lt;div>
      &lt;p>Admiral Jahas&lt;/p>
    &lt;/div>
  &lt;/td>
  &lt;td>
    &lt;div>
      &lt;div>&lt;span>Disabled&lt;/span>&lt;/div>
    &lt;/div>
  &lt;/td>
  &lt;td>
    &lt;p>2025-03-29 08:16:40&lt;/p>
  &lt;/td>
  &lt;td>&lt;button>Delete&lt;/button>&lt;/td>
&lt;/tr>
</pre>



<p>The <strong>important </strong>thing to mention here is that <code>respondHtml</code> requires us to respond with whole body! So, to bypass that, we use the <code>respondText</code> function and set the content type as HTML. </p>



<p>However, when we open up the browser, we can see this:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="388" height="362" src="http://blog.codersee.com/wp-content/uploads/2025/03/image-3.png" alt="Image is a screenshot from htmx POST invocation with invalid target." class="wp-image-18512868" srcset="https://blog.codersee.com/wp-content/uploads/2025/03/image-3.png 388w, https://blog.codersee.com/wp-content/uploads/2025/03/image-3-300x280.png 300w" sizes="auto, (max-width: 388px) 100vw, 388px" /></figure>



<p>And I am pretty sure that is not what we wanted 😀 </p>



<h3 class="wp-block-heading" id="h-htmx-target">htmx Target</h3>



<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 <code>hx-target</code> attribute that takes the CSS selector, or:</p>



<ul class="wp-block-list">
<li><code>this</code> keyword- to refer to the element with <code>hx-target</code> attribute</li>



<li><code>closest</code>, <code>next</code>, <code>previous</code> <code>&lt;CSS selector&gt;</code> (like <code>closest div</code>)- to target the closest ancestor element or itself</li>



<li><code>find &lt;CSS selector</code> &#8211; to target the first child descendant element that matches the given CSS selector</li>
</ul>



<p>As a proof, let&#8217;s take a look at what happened previously: </p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="345" height="137" src="http://blog.codersee.com/wp-content/uploads/2025/03/image-4.png" alt="" class="wp-image-18512871" srcset="https://blog.codersee.com/wp-content/uploads/2025/03/image-4.png 345w, https://blog.codersee.com/wp-content/uploads/2025/03/image-4-300x119.png 300w" sizes="auto, (max-width: 345px) 100vw, 345px" /></figure>



<p>As we can see, the table row was inserted inside the <code>form</code>. And that does not make sense, at all. </p>



<p>So, to fix that, let&#8217;s target the <code>table tbody</code> instead: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">fun FlowContent.insertUserForm() {
    div {
        form {
            attributes["hx-post"] = "/users"
            attributes["hx-target"] = "#users-table"</pre>



<p>As a result, all the other rows are deleted, but it seems to be closer to what we want:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="379" height="249" src="http://blog.codersee.com/wp-content/uploads/2025/03/image-5.png" alt="" class="wp-image-18512872" srcset="https://blog.codersee.com/wp-content/uploads/2025/03/image-5.png 379w, https://blog.codersee.com/wp-content/uploads/2025/03/image-5-300x197.png 300w" sizes="auto, (max-width: 379px) 100vw, 379px" /></figure>



<h3 class="wp-block-heading" id="h-swapping-in-htmx">Swapping in htmx</h3>



<p><strong>Next lesson:</strong> by default, <strong>htmx replaces the innerHTML of the target element</strong>. </p>



<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 <code>hx-swap</code> so the <code>tbody</code> inner HTML was deleted, and our returned one was inserted instead. </p>



<p>So, we must add the <code>hx-swap</code> with one of the following values:</p>



<ul class="wp-block-list">
<li><code>innerHTML</code>&#8211; puts the content inside the target element</li>



<li><code>outerHTML</code>&#8211; replaces the entire target element with the returned content</li>



<li><code>afterbegin</code>&#8211; prepends the content before the first child inside the target</li>



<li><code>beforebegin</code>&#8211; prepends the content before the target in the target’s parent element</li>



<li><code>beforeend</code>&#8211; appends the content after the last child inside the target</li>



<li><code>afterend</code>&#8211; appends the content after the target in the target’s parent element</li>



<li><code>delete</code>&#8211; deletes the target element regardless of the response</li>



<li><code>none</code>&#8211; does not append content from response (Out of Band Swaps and Response Headers will still be processed)</li>
</ul>



<p>And in our case, the <code>beforeend</code> is the one we should pick to append the created user at the end of the list:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">fun FlowContent.insertUserForm() {
    div {
        form {
            attributes["hx-post"] = "/users"
            attributes["hx-target"] = "#users-table"
            attributes["hx-swap"] = "beforeend"</pre>



<p>When we restart the app, everything works fine! 🙂 </p>



<h3 class="wp-block-heading" id="h-dynamic-htmx-tags-in-kotlin">Dynamic htmx Tags in Kotlin</h3>



<p>At this point, we know how to display and add new users with htmx. So, let&#8217;s learn how to delete them.</p>



<p>As the first step, let&#8217;s prepare a Ktor handler inside the <code>route("/users")</code> for the DELETE request:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">delete("/{id}") {
    val id = call.parameters["id"]!!
    userRepository.delete(id)
    call.respond(HttpStatusCode.OK)
}</pre>



<p>With that code, whenever a <code>DELETE /users/{some-id}</code> is made, we remove the user from our list and return 200 OK.</p>



<p><strong>Important lesson here:</strong> for simplicity, we return <code>200 OK</code> (and not <code>204 No Content</code>), because by default, htmx ignores successful responses other than 200.</p>



<p>Following, let&#8217;s update our button: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">td {
        button {
            attributes["hx-delete"] = "/users/${user.id}"
            attributes["hx-swap"] = "outerHTML"
            attributes["hx-target"] = "closest tr"</pre>



<p>So, firstly, whenever we generate our button, we use <strong>Kotlin string interpolation</strong> to put the user identifier in the <code>hx-delete</code> attribute value. A neat and easy way to achieve that with Kotlin. </p>



<p>When it comes to swapping, we want to find the closest <code>tr</code> parent and swap the entire element with the response. And as the response contains <strong>nothing</strong>, it will be simply removed😉</p>



<p>After we rerun the application, we will see everything working perfectly fine!</p>



<h3 class="wp-block-heading" id="h-error-handling-with-ktor-and-htmx">Error Handling with Ktor and htmx</h3>



<p>Following, let&#8217;s learn how we can handle any Ktor error response in htmx. </p>



<p>For that purpose, let&#8217;s update the POST handler in Ktor:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">post {
    val formParams = call.receiveParameters()
    val firstName = formParams["first-name"]!!
    val lastName = formParams["last-name"]!!
    val enabled = formParams["enabled-radio"]!!.toBoolean()
    if (firstName.isBlank() || lastName.isBlank())
        return@post call.respond(HttpStatusCode.BadRequest)
... the rest of the code</pre>



<p>With that validation, whenever <code>first-name</code> or <code>last-name</code> form parameter is blank, the API client receives <code>400 Bad Request</code>.</p>



<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>



<p>Well, unfortunately (or fortunately?), htmx does not provide any handling out-of-the-box.</p>



<p>But, it throws two events:</p>



<ul class="wp-block-list">
<li><code>htmx:responseError</code>&#8211; in the event of an error response from the server, like <code>400 Bad Request</code></li>



<li><code>htmx:sendError</code>&#8211; in case of connection error</li>
</ul>



<p>So, let&#8217;s add a tiny bit of JS in Kotlin, then: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">private fun BODY.insertErrorHandlingScripts() {
    script {
        +"""
            document.body.addEventListener('htmx:responseError', function(evt) {
              alert('An error occurred! HTTP status:' + evt.detail.xhr.status);
            });
            
            document.body.addEventListener('htmx:sendError', function(evt) {
              alert('Server unavailable!');
            });
        """.trimIndent()
    }
}</pre>



<p>And let&#8217;s add this script at the end of the body when rendering the homepage:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">fun HTML.renderIndex(userRepository: UserRepository) {
    head {
        script {
            src = "https://unpkg.com/htmx.org@2.0.4"
        }
    }
    body {
        div {
            insertHeader()
            insertUserForm()
            insertUserTable(userRepository.findAll())
        }
        insertErrorHandlingScripts()
    }
}</pre>



<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>



<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>



<h2 class="wp-block-heading" id="h-returning-images-in-ktor">Returning Images in Ktor</h2>



<p>Before we head to the Tailwind CSS part, let&#8217;s learn one more thing in Ktor: static responses handling.</p>



<p>So, let&#8217;s put the below image in the <code>resources -&gt; img</code> directory:</p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="500" height="500" src="http://blog.codersee.com/wp-content/uploads/2025/03/placeholder.png" alt="Image is a placeholder used ad an example in our project and shows Codersee logo." class="wp-image-18512878" style="width:100px" srcset="https://blog.codersee.com/wp-content/uploads/2025/03/placeholder.png 500w, https://blog.codersee.com/wp-content/uploads/2025/03/placeholder-300x300.png 300w, https://blog.codersee.com/wp-content/uploads/2025/03/placeholder-150x150.png 150w" sizes="auto, (max-width: 500px) 100vw, 500px" /></figure>



<p>And let&#8217;s add this image as a placeholder to each row in our table: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">fun TR.insertUserRowCells(user: User) {
    td {
        div {
            img {
                src = "/img/placeholder.png"
            }</pre>



<p>When we rerun the application, we can see that it does not work.</p>



<p>Well, to fix that, we must instruct Ktor to serve our resources as static content:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">fun Application.configureRouting(userRepository: UserRepository) {
    routing {
        staticResources("/img", "img")</pre>



<p>This time, when we restart the application, we see that placeholders are working fine. </p>



<p>And we will style them in a moment 😉 </p>



<h2 class="wp-block-heading" id="h-styling-with-tailwind-css">Styling With Tailwind CSS</h2>



<p>At this point, we have a fully working Kotlin and htmx integration.</p>



<p>So, if we already did something else than the JSON response, let&#8217;s make it nice😄</p>



<h3 class="wp-block-heading" id="h-import-tailwind">Import Tailwind</h3>



<p>Just like with htmx, let&#8217;s use the CDN to import Tailwind to the project:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">fun HTML.renderIndex(userRepository: UserRepository) {
    head {
        script {
            src = "https://unpkg.com/htmx.org@2.0.4"
        }
        script {
            src = "https://unpkg.com/@tailwindcss/browser@4"
        }
    }</pre>



<h3 class="wp-block-heading" id="h-update-index">Update Index </h3>



<p>Then, let&#8217;s navigate to the <code>Index.kt</code> and add adjustments:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">fun HTML.renderIndex(userRepository: UserRepository) {
    head {
        script {
            src = "https://unpkg.com/htmx.org@2.0.4"
        }
        script {
            src = "https://unpkg.com/@tailwindcss/browser@4"
        }
    }
    body {
        div {
            classes = setOf("m-auto max-w-5xl w-full overflow-hidden")
            insertHeader()
            insertUserForm()
            insertUserTable(userRepository.findAll())
        }
        insertErrorHandlingScripts()
    }
}
private fun FlowContent.insertHeader() {
    h5 {
        classes =
            setOf("py-8 block font-sans text-xl antialiased font-semibold leading-snug tracking-normal text-blue-gray-900")
        +"Users list"
    }
}
private fun BODY.insertErrorHandlingScripts() {
    script {
        +"""
            document.body.addEventListener('htmx:responseError', function(evt) {
              alert('An error occurred! HTTP status:' + evt.detail.xhr.status);
            });
            
            document.body.addEventListener('htmx:sendError', function(evt) {
              alert('Server unavailable!');
            });
        """.trimIndent()
    }
}</pre>



<p>As we can see, we can use the <code>classes</code> in Kotlin HTML DSL to prive classes names as a Set of String values:</p>



<p>classes = setOf(&#8220;m-auto max-w-5xl w-full overflow-hidden&#8221;)</p>



<p>In my case, I prefer simply copy-pasting those values instead of separating them with colons.</p>



<h2 class="wp-block-heading" id="h-refactor-form">Refactor Form</h2>



<p>Then, let&#8217;s update the <code>Form.kt</code>: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">fun FlowContent.insertUserForm() {
    div {
        classes = setOf("mx-auto w-full")
        form {
            attributes["hx-post"] = "/users"
            attributes["hx-target"] = "#users-table"
            attributes["hx-swap"] = "beforeend"
            div {
                classes = setOf("-mx-3 flex flex-wrap")
                div {
                    classes = setOf("w-full px-3 sm:w-1/4")
                    label {
                        classes = setOf("mb-3 block text-base font-medium text-[#07074D]")
                        htmlFor = "first-name"
                        +"First Name"
                    }
                    input {
                        classes =
                            setOf("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")
                        type = InputType.text
                        name = "first-name"
                        id = "first-name"
                        placeholder = "First Name"
                    }
                }
                div {
                    classes = setOf("w-full px-3 sm:w-1/4")
                    label {
                        classes = setOf("mb-3 block text-base font-medium text-[#07074D]")
                        htmlFor = "last-name"
                        +"Last Name"
                    }
                    input {
                        classes =
                            setOf("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")
                        type = InputType.text
                        name = "last-name"
                        id = "last-name"
                        placeholder = "Last Name"
                    }
                }
                div {
                    classes = setOf("w-full px-3 sm:w-1/4")
                    label {
                        classes = setOf("mb-3 block text-base font-medium text-[#07074D]")
                        +"Account enabled"
                    }
                    div {
                        classes = setOf("flex items-center space-x-6 pt-3")
                        div {
                            classes = setOf("flex items-center")
                            input {
                                classes = setOf("h-5 w-5")
                                type = InputType.radio
                                name = "enabled-radio"
                                id = "radio-button-1"
                                value = "true"
                            }
                            label {
                                classes = setOf("pl-3 text-base font-medium text-[#07074D]")
                                htmlFor = "radio-button-1"
                                +"Yes"
                            }
                        }
                        div {
                            classes = setOf("flex items-center")
                            input {
                                classes = setOf("h-5 w-5")
                                type = InputType.radio
                                name = "enabled-radio"
                                id = "radio-button-2"
                                value = "false"
                                checked = true
                            }
                            label {
                                classes = setOf("pl-3 text-base font-medium text-[#07074D]")
                                htmlFor = "radio-button-2"
                                +"No"
                            }
                        }
                    }
                }
                div {
                    classes = setOf("w-full px-3 sm:w-1/4 pt-8")
                    button {
                        classes =
                            setOf("cursor-pointer rounded-md bg-slate-800 py-3 px-8 text-center text-base font-semibold text-white outline-none")
                        +"Add user"
                    }
                }
            }
        }
    }
}</pre>



<p>Similarly, we don&#8217;t change anything in here apart from adding a bunch of classes. A whooooole bunch of classes 🙂 </p>



<h2 class="wp-block-heading" id="h-modify-user-table">Modify User Table</h2>



<p>As the last step, let&#8217; apply changes to our user table: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">fun FlowContent.insertUserTable(users: List&lt;User>) {
    div {
        classes = setOf("px-0 overflow-scroll")
        table {
            classes = setOf("w-full mt-4 text-left table-auto min-w-max")
            thead {
                tr {
                    th {
                        classes = setOf("p-4 border-y border-blue-gray-100 bg-blue-gray-50/50")
                        +"User"
                    }
                    th {
                        classes = setOf("p-4 border-y border-blue-gray-100 bg-blue-gray-50/50")
                        +"Status"
                    }
                    th {
                        classes = setOf("p-4 border-y border-blue-gray-100 bg-blue-gray-50/50")
                        +"Created At"
                    }
                    th {
                        classes = setOf("p-4 border-y border-blue-gray-100 bg-blue-gray-50/50")
                    }
                }
            }
            tbody {
                id = "users-table"
                users.forEach { user ->
                    tr {
                        insertUserRowCells(user)
                    }
                }
            }
        }
    }
}
fun TR.insertUserRowCells(user: User) {
    td {
        classes = setOf("p-4 border-b border-blue-gray-50")
        div {
            classes = setOf("flex items-center gap-3")
            img {
                classes = setOf("relative inline-block h-9 w-9 !rounded-full object-cover object-center")
                src = "/img/placeholder.png"
            }
            p {
                classes = setOf("block font-sans text-sm antialiased font-normal leading-normal text-blue-gray-900")
                +"${user.firstName} ${user.lastName}"
            }
        }
    }
    td {
        classes = setOf("p-4 border-b border-blue-gray-50")
        div {
            classes = setOf("w-max")
            div {
                val enabledLabel = if (user.enabled) "Enabled" else "Disabled"
                val labelColor = if (user.enabled) "green" else "red"
                classes =
                    setOf("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")
                span { +enabledLabel }
            }
        }
    }
    td {
        classes = setOf("p-4 border-b border-blue-gray-50")
        p {
            classes = setOf("block font-sans text-sm antialiased font-normal leading-normal text-blue-gray-900")
            +user.createdAt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
        }
    }
    td {
        classes = setOf("p-4 border-b border-blue-gray-50")
        button {
            classes =
                setOf("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")
            attributes["hx-delete"] = "/users/${user.id}"
            attributes["hx-swap"] = "outerHTML"
            attributes["hx-target"] = "closest tr"
            unsafe {
                +"""
                    &lt;span class="absolute transform -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2">
                        &lt;svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
                             stroke-width="2" stroke="currentColor" class="w-6 h-6">
                            &lt;path stroke-linecap="round" stroke-linejoin="round"
                                  d="M6 18L18 6M6 6l12 12"/>
                        &lt;/svg>
                    &lt;/span>
                """.trimIndent()
            }
        }
    }
}</pre>



<p>And here, apart from the CSS classes, we also added the <code>X</code> icon with the <code>unsafe</code> function from Kotlin HTML DSL:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">unsafe {
    +"""
        &lt;span class="absolute transform -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2">
            &lt;svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
                 stroke-width="2" stroke="currentColor" class="w-6 h-6">
                &lt;path stroke-linecap="round" stroke-linejoin="round"
                      d="M6 18L18 6M6 6l12 12"/>
            &lt;/svg>
        &lt;/span>
    """.trimIndent()
}</pre>



<p>And voila! When we run the application now, we should see a pretty decent-looking UI😉</p>



<h2 class="wp-block-heading" id="h-summary">Summary</h2>



<p>That&#8217;s all for this tutorial on how to work with htmx and Kotlin HTML DSL in Ktor.</p>



<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>



<p>▶️ Over 15 hours of video content divided into over 130 lessons</p>



<p>🛠️ Hands-on approach: together, we implement 4 actual services</p>



<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>



<p>Lastly, you can find the source code for this lesson in <a href="https://github.com/codersee-blog/ktor-htmx" target="_blank" rel="noreferrer noopener">this GitHub repository</a>.</p>
<p>The post <a href="https://blog.codersee.com/quick-quide-to-htmx-kotlin/">A Quick Guide to htmx in Kotlin</a> appeared first on <a href="https://blog.codersee.com">Codersee blog- Kotlin on the backend</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.codersee.com/quick-quide-to-htmx-kotlin/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 

Served from: blog.codersee.com @ 2026-05-16 18:33:31 by W3 Total Cache
-->