Ktor and Thymeleaf work well together
In my article “Switching from Spring Boot to Ktor”, I showed how easy an API endpoint can be developed using the Ktor framework (in combination with Koin and Exposed).
Developing an application which is only accessed by a REST API client is one requirement, but what if I want to provide a graphical interface for the user? How can Ktor help me with this?
Ktor provides support for JVM templating engines. So it is possible for me to use one of the well known engines I know from my Java development.
With this article I show how to develop a simple user Interface which supports basic CRUD operations using Ktor and Thymeleaf.
The starting point is the “New Project” wizard of Intellij:
I add the Routing and Thymeleaf plugins to the project.
The created project contains the following code parts which provide the functionality for a sample page:
When the server is started successfully, I can browse the sample page which shows me a user name.
So let’s start adding functionality.
As a first step I create the domain objects which represent my business use case. I use 2 classes which are related to each other. There is a customer who has an address. For simplicity every customer has his/her own address object.
To be able to have a first visible page showing the domain objects I create an hardcoded list of domain objects, which are hold in-memory. and can be shown in a table.
I edit the sample
index.html to show the sample data in a html table.
Starting the server and browsing the
index.html, the result looks not very pretty, even for me as mainly backend developer.
To improve the design with asmall effort, I add bootstrap (https://getbootstrap.com/docs/5.2/getting-started/introduction/) to the project. With this it looks much better now.
Until now I have only a static page which is showing my hard-coded static content. The next step is to add functionality for the creation of new customer.
To be able to create new customers, I have to do the following changes:
- Extract the general header part of the html page to a fragment.
- Add the navigation to index.html page allowing to navigate to newcustomer.html page
- Add the newcustomer.html page
- Add a button to save a newly created customer and navigate back to the
I add the
newcustomer.html page and can see that the header with the bootstrap configuration is duplicated. To prevent adding this part again and again, I extract this part to a fragment. Thymeleaf allows to extract common functionality to a fragment which can be used across different pages.
To be able to add the data for a new customer, I have to add a new page and a button on the
index.html page to navigate there.
For the navigation I add a link which has a
th:href attribute with an URL relative to the root (localhost:8080 by default). You can look for additional information about how to work with links in Thymeleaf in the documentation.
When the link is clicked a request is send with the URL of
http:localhost:8080/new. I need an endpoint in my routing section which is handling these requests.
For the deserialization of the form data to object I need to add content negotiation to the application.
Ktor is currently not supporting the deserialization of form data to object (like it is possible when using Spring) out of the box. When using
I get an error which is informing me that transformation of request content to DTO object is not possible:
Cannot transform this request’s content to com.poisonedyouth.CustomerData
The data which is returned from the HTTP post request has the following form:
So there are 2 options for me to proceed:
- Write a custom deserializer which is transforming the form data to object.
- Retrieve the form data as String and use
ObjectMapperto transform to object manually.
To keep it simple I use the second option:
I retrieve the form data as String and split the content to get key-value pairs. The resulting map can be converted by
ObjectMapper of Jackson to my DTO object.
With this changes it is possible to create new customer and return back to the
To be able to edit an existing customer from index.html page, I have to add a button in the customer table which navigates to the edit customer page where I can update the existing data.
The following changes have to be done:
- Add the editcustomer.html page
- Extract general customer input form to fragment
- Add a button to navigate from index.html page to editcustomer.html page
- Add button to save updated customer and navigate back to index.html
As the first step I add the new editcustomer.html page which looks nearly exactly the same as the newcustomer.html page. It is just the
th:action attribute which has a different URL destination.
To not duplicate the whole form I extract the code to a fragment and specify variables for
th:object which can be set by implementing pages.
With this form fragment the both pages look very tidy.
To finalize the task for editing an existing customer, I need to add a link to the
index.html which is navigating to the
editcustomer.html page and passing the customerId of the selected customer in the URL.
Templating.kt class I extract the
customerId of the request path and search for the corresponding customer in the list and put the data to the model for the
After finishing this changes it is possible to click on edit button in the row of a customer and edit the data in the input form.
Deleting an existing customer from table of
index.html page, requires to have a button in the customer table which triggers a request that removes the customer from the list of existing customers.
The following changes have to be done:
- Add button to delete customer and reload the page
I need to add a link to the index.html which is navigating to a URL which contains the
customerId of the selected customer.
At the moment it is only possible to send GET and POST requests from an HTML form. So I need to abuse the GET request to send the customerId which is handled by routing endpoint.
Until now the
index.html looks like the following:
It is possible to create new customers, edit customers, and to delete the existing customer. Also, the navigation between the pages is working.
But every time the application is starting I start with the same initial static data. To have a persistent list of customer I need to add a database.
To keep things simple I use Exposed + H2 database.
With the following configuration it is possible to use both in my application.
I replace the usage of my in-memory
DataHolder by a persistence layer which is using a H2 file database. The basic CRUD functionality I implement using the Entity classes of Exposed DAO.
DataHolder is acting as a service layer which is decoupling the persistence layer from the rest of the application (making it possible to exchange Exposed by other persistence framework).
With this changes I have got a simple CRUD application providing a basic user interface using templating engine of Thymeleaf.
Thymeleaf is providing a lot more functionality. A good overview can be found in official documentation.
Because I’m not very happy with the solution of deserialization of the form content which is sent from
Thymeleaf I tried to replace the current solution of manually mapping the form content to my DTO object by a more generic variant. I can register a custom
ContentConverter for the form data.
When the form is submitted I get the following information in the
- the used charset
typeInfocontaining the information about the used model class
- the content as
To have a generic solution which is able to deserialize form data to different model classes it’s necessary to deal with Kotlins reflection mechanism. This caused me a lot of additional work to solve this. So I decided to put this to an extra article which I will publish in the future.
Ktor and Thymeleaf work well together but compared to Spring Boot with Thymeleaf — I might’ve missed some crucial functionality like the deserialization mechanism or the ability to send “delete” form requests out of the box. Here’s the official documentation to refer to for Ktor and Thymeleaf.
If you are interested in the code of my application, you can find the repository on GitHub: