Ktor 3.0.0-beta-1 Help

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

  1. Navigate to the Ktor Project Generator.

  2. In the Project artifact field, enter com.example.ktor-task-web-app as the name of your project artifact.

    Ktor Project Generator project artifact name

  3. In the next screen, search for and add the following plugins by clicking on the Add button:

    • Routing

    • Static Content

    • Thymeleaf

    Adding plugins in the Ktor Project Generator
    Once you have added the plugins, you will see all three plugins listed below the project settings.
    Ktor Project Generator plugins list

  4. Click the Download button to generate and download your Ktor project.

Add starter code

  1. Open your project in IntelliJ IDEA or another IDE of your choice.

  2. Navigate to src/main/kotlin/com/example and create a subpackage called model.

  3. Inside the model package, create a new Task.kt file.

  4. In the Task.kt file, add an enum to represent priorities and a data 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 this case, our goal is to create a server page that can write the contents of tasks to the browser.

  5. Open the Templating.kt file within the plugins package.

  6. 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. The ThymeleafContent 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 path src/main/resources/templates/thymeleaf/all-tasks.html

  7. Navigate to src/main/resources/templates/thymeleaf and create a new all-tasks.html file.

  8. 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>
  9. In IntelliJ IDEA, click on the run button (intelliJ IDEA run icon) to start the application.

  10. 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:

    A web browser window displaying a list of tasks

    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:

static("/static") { resources("static") }

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

  1. 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>
  2. In IntelliJ IDEA, click the rerun button (intelliJ IDEA rerun icon) to restart the application.

  3. 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:

    A web browser displaying an HTML form

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

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.

    1. Navigate to the Templating.kt file in src/main/kotlin/com/example/plugins.

    2. 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 parameter taskName 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.

    3. Navigate to src/main/resources/templates/thymeleaf and create a new single-task.html file.

    4. 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>
    5. In the same folder, create a new file called tasks-by-priority.html.

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

    1. Navigate to the Templating.kt file in src/main/kotlin/com/example/plugins.

    2. Add the following post request route within the configureTemplating() 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) } }
    3. In IntelliJ IDEA, click the rerun button (intelliJ IDEA rerun icon) to restart the application.

    4. Navigate to http://0.0.0.0:8080/static/index.html in your browser.

    5. Enter new task details in the Create or edit a task form.

      A web browser displaying HTML forms
    6. Click on the Submit button to submit the form. You will then see the new task displayed in a list of all tasks:

      A web browser displaying a list of 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.

    Last modified: 03 July 2024