In this tutorial, you will learn how to build an interactive website in Kotlin with Ktor and Thymeleaf templates.
In the previous tutorial, you learned how to create a RESTful service, to be consumed by a Single Page Application (SPA) written in JavaScript. Although this is a very popular architecture, it does not suit every project.
There are many reasons why you may wish to keep all the implementation on the server and only send markup to the client, such as the following:
Simplicity – to maintain a single codebase.
Security – to prevent placing data or code onto the browser that could provide insight to attackers.
Supportability – to allow clients to use as wide a range of clients as possible, including legacy browsers and those with JavaScript disabled.
You can do this tutorial independently, however, we strongly recommend that you complete the preceding tutorial to learn how to create RESTful APIs.
We recommend that you install IntelliJ IDEA, but you could use another IDE of your choice.
Hello Task Manager web application
In this tutorial, you'll transform the task management application you built in the previous tutorial into a web application. To do this, you'll use several Ktor plugins.
While you can manually add these plugins to your existing project, it’s easier to generate a new project and gradually incorporate code from the previous tutorial. We'll provide all the necessary code along the way, so you don’t need to have the previous projects to hand.
In the Project artifact field, enter com.example.ktor-task-web-app as the name of your project artifact.
On the next screen, search for and add the following plugins by clicking on the Add button:
Static Content
Thymeleaf
Once you have added the plugins, you will see all three plugins listed below the project settings.
Click the Download button to generate and download your Ktor project.
Add starter code
Open your project in IntelliJ IDEA or another IDE of your choice.
Navigate to src/main/kotlin and create a subpackage called model.
Inside the model package, create a new Task.kt file.
In the Task.kt file, add an enum to represent priorities and a data class to represent tasks:
package com.example.model
enum class Priority {
Low, Medium, High, Vital
}
data class Task(
val name: String,
val description: String,
val priority: Priority
)
Once again, you want to create Task objects and send them to clients in a form that can be displayed.
In the Create RESTful APIs tutorial, you annotated the Task class with the Serializable type from the kotlinx.serialization library.
In this case, the goal is to create a server page that writes the contents of tasks to the browser.
Open the Routing.kt file in src/main/kotlin.
In the .configureRouting() function, add a route for /tasks as shown below:
fun Application.configureRouting() {
routing {
get("/") {
call.respondText("Hello, World!")
}
get("/html-thymeleaf") {
call.respond(ThymeleafContent("index", mapOf("user" to ThymeleafUser(1, "user1"))))
}
// Add this additional route
get("/tasks") {
val tasks = listOf(
Task("cleaning", "Clean the house", Priority.Low),
Task("gardening", "Mow the lawn", Priority.Medium),
Task("shopping", "Buy the groceries", Priority.High),
Task("painting", "Paint the fence", Priority.Medium)
)
call.respond(ThymeleafContent("all-tasks", mapOf("tasks" to tasks)))
}
staticResources("/static", "static")
}
}
When the server receives a request for /tasks, it creates a list of tasks and then passes it to a Thymeleaf template. The ThymeleafContent type takes the name of the template to be triggered and a table of values to be accessible on the page.
Open the Thymeleaf.kt file in src/main/kotlin.
You should see the following .configureThymeleaf function:
Within the initialization of the Thymeleaf plugin, Ktor looks inside the templates/thymeleaf folder for server pages. As with static content, it expects this folder to be inside the resources directory. It also expects a .html suffix.
In this case, the name all-tasks maps to the path src/main/resources/templates/thymeleaf/all-tasks.html
Navigate to the src/main/resources and create a new templates/thymeleaf directory.
Within src/main/resources/templates/thymeleaf, create a new all-tasks.html file.
Open the all-tasks.html file and add the content below:
In IntelliJ IDEA, click on the run button () to start the application.
Navigate to http://0.0.0.0:8080/tasks in your browser. You should see all current tasks displayed in a table, as shown below:
Like all server-page frameworks, Thymeleaf templates mix static content (to be sent to the browser) with dynamic content (to be executed on the server). If you had chosen an alternative framework, such as Freemarker, you could have provided the same functionality with a slightly different syntax.
Add the GET routes
Now that you're familiar with the process of requesting a server page, continue with transferring the functionality from the previous tutorials into this one.
Because you included the Static Content plugin, the following code will be present in the Routing.kt file:
staticResources("/static", "static")
This means that, for example, a request to /static/index.html is served content from the following path:
src/main/resources/static/index.html
As this file is already part of the generated project, you can use it as a home page for the functionality you wish to add.
Reuse the index page
Open the index.html file within src/main/resources/static and replace its contents with the implementation below:
In IntelliJ IDEA, click the rerun button () to restart the application.
Navigate to http://localhost:8080/static/index.html in your browser. You should see a link button and three HTML forms that allow you to view, filter and create tasks:
Note that when you are filtering tasks by name or priority, you are submitting an HTML form through a GET request. This means that the parameters are added to the query string after the URL.
For example, if you search for tasks of Medium priority, the request sent to the server looks like the following:
The repository for tasks can remain identical to the one from the previous tutorial.
Inside the model package create a new TaskRepository.kt file and add the code below:
package com.example.model
object TaskRepository {
private val tasks = mutableListOf(
Task("cleaning", "Clean the house", Priority.Low),
Task("gardening", "Mow the lawn", Priority.Medium),
Task("shopping", "Buy the groceries", Priority.High),
Task("painting", "Paint the fence", Priority.Medium)
)
fun allTasks(): List<Task> = tasks
fun tasksByPriority(priority: Priority) = tasks.filter {
it.priority == priority
}
fun taskByName(name: String) = tasks.find {
it.name.equals(name, ignoreCase = true)
}
fun addTask(task: Task) {
if (taskByName(task.name) != null) {
throw IllegalStateException("Cannot duplicate task names!")
}
tasks.add(task)
}
}
Reuse routes for GET requests
Now that you have created the repository, you can implement the routes for GET requests.
Navigate to the Routing.kt file in src/main/kotlin.
Replace the current version of .configureRouting() with the implementation below:
fun Application.configureRouting() {
routing {
get("/") {
call.respondText("Hello, World!")
}
get("/html-thymeleaf") {
call.respond(ThymeleafContent("index", mapOf("user" to ThymeleafUser(1, "user1"))))
}
staticResources("/static", "static")
route("/tasks") {
get {
val tasks = TaskRepository.allTasks()
call.respond(
ThymeleafContent("all-tasks", mapOf("tasks" to tasks))
)
}
get("/byName") {
val name = call.request.queryParameters["name"]
if (name == null) {
call.respond(HttpStatusCode.BadRequest)
return@get
}
val task = TaskRepository.taskByName(name)
if (task == null) {
call.respond(HttpStatusCode.NotFound)
return@get
}
call.respond(
ThymeleafContent("single-task", mapOf("task" to task))
)
}
get("/byPriority") {
val priorityAsText = call.request.queryParameters["priority"]
if (priorityAsText == null) {
call.respond(HttpStatusCode.BadRequest)
return@get
}
try {
val priority = Priority.valueOf(priorityAsText)
val tasks = TaskRepository.tasksByPriority(priority)
if (tasks.isEmpty()) {
call.respond(HttpStatusCode.NotFound)
return@get
}
val data = mapOf(
"priority" to priority,
"tasks" to tasks
)
call.respond(ThymeleafContent("tasks-by-priority", data))
} catch (ex: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest)
}
}
}
}
}
The above code can be summarized as follows:
In a GET request to /tasks, the server retrieves all tasks from the repository and uses the all-tasks template to generate the next view sent to the browser.
In a GET request to /tasks/byName, the server retrieves the parameter name from the queryString, finds the matching task, and uses the single-task template to generate the next view sent to the browser.
In a GET request to /tasks/byPriority, the server retrieves the parameter priority from the queryString, finds the matching tasks, and uses the tasks-by-priority template to generate the next view sent to the browser.
For all of this to work, you need to add additional templates.
Navigate to src/main/resources/templates/thymeleaf and create a new single-task.html file.
Open the single-task.html file and add the following content: