Spring Kotlin with HTMX - Creating a Blog Application

Marco Pegoraro | Jul 28, 2026 min read

Introduction

Disclaimer: No generative AI was used to write this blog post, nor the application code.

This blog post will demonstrate how to create a blog application with the possibility to create, edit and delete posts. It will contain authentication so the admin can manage the posts and also user authentication so readers can leave coments in the posts. For that, it should be able to handle a database integration.

For this application we will use Spring Initialzr to create the scaffolding and add the following dependencies:

DependencyObservation
KotlinModern programming language that also runs in the JVM, aims to be a fun alternative to Java, also it is fully interoperable.
Spring SecurityProvides the security foundation into the application already defined and ready to use by default, only requiring the specific configuration.
HTMXJS library that allows a SPA behaivor in a traditional server application using only HTML custom tags.
FlywayProvides an easy way to manage migrations using plain SQL.
ThymeleafTemplate Engine
GraalVMGenerate the application using a native image instead of running in the JVM, saving resources and start up time.
ActuatorTo monitore the application using standarized http paths.
JPA/PostgresThe database connection.

Click here to be redirected to Spring Initialzr with the dependencies already loaded

With the application downloaded, open it in IntelliJ IDEA.

For some reason, the downloaded project was using JDK 24 and not the 25, so i’ve changed manually in the pom.xml the java.version to 25 and the kotlin.version to 2.4.0. In your case, you might not need this.

You should be able to build and run the application in this stage (it will use a default spring security password and the H2 database).

To log in, open the http://localhost:8080 url and then type the username “user” and the password that it is in the run console.

To keep it simple, lets just temporarelly coment the spring security dependency and also the open telemetry one, so we can worry only about the development of the application and then when the application starts to take form, then we add those dependencies again.

Coding the models

I will not follow the hexagonal pattern so we can make it simpler.

For the persistence layer, i will create then a package entities and another one called repositories.

For this blog application we will create in total 4 models:

entities/Post.kt:

@Entity
class Post(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null,
    @Column(nullable = false)
    var title: String = "",
    var subtitle: String? = null,
    @Column(columnDefinition = "TEXT")
    var body: String? = null,
    @Column(nullable = false)
    var isDraft: Boolean = true,
    @Column(nullable = false)
    var language: String = "en",
    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "post_tag",joinColumns = [ JoinColumn(name = "post_id")], inverseJoinColumns = [JoinColumn(name = "tag_id")],uniqueConstraints = [UniqueConstraint(name = "uk_post_tag",columnNames = ["post_id", "tag_id"])])
    var tags: MutableSet<Tag> = mutableSetOf(),
    @CreationTimestamp
    @Column(nullable = false, updatable = false)
    var createdAt: OffsetDateTime? = null,
    var publishedAt: OffsetDateTime? = null,
    @UpdateTimestamp
    var updatedAt: OffsetDateTime? = null
)

entities/Tag.kt:

@Entity
class Tag(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null,
    @Column(nullable = false)
    var name: String = "",
    @ManyToMany(mappedBy = "tags", fetch = FetchType.LAZY)
    var posts: MutableSet<Post> = mutableSetOf(),
    @CreationTimestamp
    @Column(nullable = false, updatable = false)
    var createdAt: OffsetDateTime? = null,
    @UpdateTimestamp
    var updatedAt: OffsetDateTime? = null
)

entities/Comment.kt:

@Entity
class Comment(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null,
    @Column(nullable = false, columnDefinition = "TEXT")
    var body: String = "",
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "post_id", nullable = false)
    var post: Post? = null,
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "author_id", nullable = false)
    var author: AppUser? = null,
    @CreationTimestamp
    @Column(nullable = false, updatable = false)
    var createdAt: OffsetDateTime? = null,
    @UpdateTimestamp
    var updatedAt: OffsetDateTime? = null
)

entities/AppUser.kt:

@Entity
class AppUser(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null,
    @Column(nullable = false, unique = true)
    var username: String = "",
    @Column(nullable = false)
    var passwordHash: String = "",
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    var role: Role = Role.USER,
    @Column(nullable = false)
    var enabled: Boolean = true
)

// And this enum in other file inside the same package:
enum class Role {
    USER,
    ADMIN
}

Also create the repositories:

repositories/PostRepository.kt:

@Repository
interface PostRepository : JpaRepository<Post, Long> {
    fun findAllByIsDraftFalse(pageable: Pageable): Page<Post>
}

repositories/TagRepository.kt:

@Repository
interface TagRepository : JpaRepository<Tag, Long>

repositories/CommentRepository.kt:

@Repository
interface CommentRepository : JpaRepository<Comment, Long>

repositories/AppUserRepository.kt:

@Repository
interface AppUserRepository : JpaRepository<AppUser, Long>