Create a website in Kotlin with Ktor
In this tutorial, we will show you 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, which we assumed would 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.
Ktor supports this approach by integrating with several server-page technologies.
Prerequisites
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 Manager you built in the previous tutorial into a Web Application. To do this, you'll use several Ktor plugins.
While you could 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.
Create the initial project with plugins
Navigate to the Ktor Project Generator.
In the Project artifact field, enter com.example.ktor-task-web-app as the name of your project artifact.
In the next screen, search for and add the following plugins by clicking on the Add button:
Routing
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/com/example 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 adata class
to represent tasks:enum class Priority { Low, Medium, High, Vital } data class Task( val name: String, val description: String, val priority: Priority )Once again, we want to create
Task
objects and send them to clients in a form that can be displayed.You may recall that:
In the Handle requests and generate responses tutorial we added handwritten extension functions to convert Tasks into HTML.
In the Create RESTful APIs tutorial we annotated the
Task
class with theSerializable
type from thekotlinx.serialization
library.
In this case, our goal is to create a server page that can write the contents of tasks to the browser.
Open the Templating.kt file within the plugins package.
In the
configureTemplating()
method, add a route for/tasks
as shown below:fun Application.configureTemplating() { install(Thymeleaf) { setTemplateResolver(ClassLoaderTemplateResolver().apply { prefix = "templates/thymeleaf/" suffix = ".html" characterEncoding = "utf-8" }) } routing { get("/html-thymeleaf") { call.respond(ThymeleafContent( "index", mapOf("user" to ThymeleafUser(1, "user1")) )) } //this is the additional route to add 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))) } } }When the server receives a request for
/tasks
, it creates a list of tasks and then passes it to a Thymeleaf template. TheThymeleafContent
type takes the name of the template we wish to trigger and a table of values we wish to be accessible on the page.Within the initialization of the Thymeleaf plugin at the top of the method, you can see that Ktor will look inside templates/thymeleaf for server pages. As with static content, it will expect this folder to be inside the resources directory. It will also expect a .html suffix.
In this case, the name
all-tasks
will map to the pathsrc/main/resources/templates/thymeleaf/all-tasks.html
Navigate to src/main/resources/templates/thymeleaf and create a new all-tasks.html file.
Open the all-tasks.html file and add the content below:
<!DOCTYPE html > <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>All Current Tasks</title> </head> <body> <h1>All Current Tasks</h1> <table> <thead> <tr> <th>Name</th><th>Description</th><th>Priority</th> </tr> </thead> <tbody> <tr th:each="task: ${tasks}"> <td th:text="${task.name}"></td> <td th:text="${task.description}"></td> <td th:text="${task.priority}"></td> </tr> </tbody> </table> </body> </html>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 we had chosen an alternative framework, such as Freemarker, we 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:
This means that, for example, a request to /static/index.hml
will be 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/resources/static and replace its contents with the implementation below:
<html> <head> </head> <body> <h1>Task Manager Web Application</h1> <div> <h3><a href="/tasks">View all the tasks</a></h3> </div> <div> <h3>View tasks by priority</h3> <form method="get" action="/tasks/byPriority"> <select name="priority"> <option name="Low">Low</option> <option name="Medium">Medium</option> <option name="High">High</option> <option name="Vital">Vital</option> </select> <input type="submit"> </form> </div> <div> <h3>View a task by name</h3> <form method="get" action="/tasks/byName"> <input type="text" name="name" width="10"> <input type="submit"> </form> </div> <div> <h3>Create or edit a task</h3> <form method="post" action="/tasks"> <div> <label for="name">Name: </label> <input type="text" id="name" name="name" size="10"> </div> <div> <label for="description">Description: </label> <input type="text" id="description" name="description" size="20"> </div> <div> <label for="priority">Priority: </label> <select id="priority" name="priority"> <option name="Low">Low</option> <option name="Medium">Medium</option> <option name="High">High</option> <option name="Vital">Vital</option> </select> </div> <input type="submit"> </form> </div> </body> </html>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
orpriority
, you are submitting an HTML form through a GET request. This means that the parameters will be added to the query string after the URL.For example, if you search for tasks of
Medium
priority this is the request that will be sent to the server:http://localhost:8080/tasks/byPriority?priority=Medium
Reuse the TaskRepository
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:
Reuse routes for GET requests
Now that you have created the repository, you can implement the routes for GET requests.
Navigate to the Templating.kt file in src/main/kotlin/com/example/plugins.
Replace the current version of
configureTemplating()
with the implementation below:fun Application.configureTemplating() { install(Thymeleaf) { setTemplateResolver(ClassLoaderTemplateResolver().apply { prefix = "templates/thymeleaf/" suffix = ".html" characterEncoding = "utf-8" }) } routing { route("/tasks") { get { val tasks = TaskRepository.allTasks() call.respond( ThymeleafContent("all-tasks", mapOf("tasks" to tasks)) ) } get("/byName") { val name = call.request.queryParameters["taskName"] 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 parametertaskName
from thequeryString
, 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 parameterpriority
from thequeryString
, 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:
<!DOCTYPE html > <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>All Current Tasks</title> </head> <body> <h1>The Selected Task</h1> <table> <tbody> <tr> <th>Name</th> <td th:text="${task.name}"></td> </tr> <tr> <th>Description</th> <td th:text="${task.description}"></td> </tr> <tr> <th>Priority</th> <td th:text="${task.priority}"></td> </tr> </tbody> </table> </body> </html>In the same folder, create a new file called tasks-by-priority.html.
Open the tasks-by-priority.html file and add the following content:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Tasks By Priority </title> </head> <body> <h1>Tasks With Priority <span th:text="${priority}"></span></h1> <table> <thead> <tr> <th>Name</th> <th>Description</th> <th>Priority</th> </tr> </thead> <tbody> <tr th:each="task: ${tasks}"> <td th:text="${task.name}"></td> <td th:text="${task.description}"></td> <td th:text="${task.priority}"></td> </tr> </tbody> </table> </body> </html>
Add support for POST requests
Next, you will add a POST request handler to /tasks
to do the following:
Extract the information from the form parameters.
Add a new task using the repository.
Display tasks by reusing the all-tasks template.
Navigate to the Templating.kt file in src/main/kotlin/com/example/plugins.
Add the following
post
request route within theconfigureTemplating()
method:post { val formContent = call.receiveParameters() val params = Triple( formContent["name"] ?: "", formContent["description"] ?: "", formContent["priority"] ?: "" ) if (params.toList().any { it.isEmpty() }) { call.respond(HttpStatusCode.BadRequest) return@post } try { val priority = Priority.valueOf(params.third) TaskRepository.addTask( Task( params.first, params.second, priority ) ) val tasks = TaskRepository.allTasks() call.respond( ThymeleafContent("all-tasks", mapOf("tasks" to tasks)) ) } catch (ex: IllegalArgumentException) { call.respond(HttpStatusCode.BadRequest) } catch (ex: IllegalStateException) { call.respond(HttpStatusCode.BadRequest) } }In IntelliJ IDEA, click the rerun button () to restart the application.
Navigate to http://0.0.0.0:8080/static/index.html in your browser.
Enter new task details in the Create or edit a task form.
Click on the Submit button to submit the form. You will then see the new task displayed in a list of all tasks:
Next steps
Congratulations! You have now completed rebuilding your Task Manager as a web application and learned how to utilize Thymeleaf templates.
Continue to the next tutorial to learn how to work with Web Sockets.