Ktor 3.0.0 Help

Creating an interactive website

In this series of tutorials, we'll show you how to create a simple blog application in Ktor:

  • In the first tutorial, we showed how to host static content like images and HTML pages.

  • In this tutorial, we'll make our application interactive using the FreeMarker template engine.

  • Finally, we'll add persistence to our website using the Exposed framework.

Adjust FreeMarker configuration

The Ktor plugin for IntelliJ IDEA already generated code for the FreeMarker plugin in the plugins/Templating.kt file:

import freemarker.cache.* import io.ktor.server.application.* import io.ktor.server.freemarker.* fun Application.configureTemplating() { install(FreeMarker) { templateLoader = ClassTemplateLoader(this::class.java.classLoader, "templates") } }

The templateLoader setting tells our application that FreeMarker templates will be located in the templates directory. Let's also add the outputFormat as follows:

import freemarker.cache.* import freemarker.core.* import io.ktor.server.application.* import io.ktor.server.freemarker.* fun Application.configureTemplating() { install(FreeMarker) { templateLoader = ClassTemplateLoader(this::class.java.classLoader, "templates") outputFormat = HTMLOutputFormat.INSTANCE } }

The outputFormat setting helps convert control characters provided by the user to their corresponding HTML entities. This ensures that when one of our journal entries contains a String like <b>Hello</b>, it is actually printed as <b>Hello</b>, not Hello. This so-called escaping is an essential step in preventing XSS attacks.

Create a model

First, we need to create a model describing an article in our journal application. Create a models package inside com.example, add the Article.kt file inside the created package, and insert the following code:

package com.example.models import java.util.concurrent.atomic.AtomicInteger class Article private constructor(val id: Int, var title: String, var body: String) { companion object { private val idCounter = AtomicInteger() fun newEntry(title: String, body: String) = Article(idCounter.getAndIncrement(), title, body) } }

An article has three attributes: id, title, and body. The title and body attributes can be specified directly while a unique id is generated automatically using AtomicInteger - a thread-safe data structure that ensures that two articles will never receive the same ID.

Inside Article.kt, let's create a mutable list for storing articles and add the first entry:

val articles = mutableListOf(Article.newEntry( "The drive to develop!", "...it's what keeps me going." ))

Define routes

Now we are ready to define routes for our journal. Open the com/example/plugins/Routing.kt file and add the following code inside configureRouting:

fun Application.configureRouting() { routing { // ... get("/") { call.respondRedirect("articles") } route("articles") { get { // Show a list of articles } get("new") { // Show a page with fields for creating a new article } post { // Save an article } get("{id}") { // Show an article with a specific id } get("{id}/edit") { // Show a page with fields for editing an article } post("{id}") { // Update or delete an article } } } }

This code works as follows:

  • The get("/") handler redirects all GET requests made to the / path to /articles.

  • The route("articles") handler is used to group routes related to various actions: showing a list of articles, adding a new article, and so on. For example, a nested get function without a parameter responds to GET requests made to the /articles path, while get("new") responds to GET requests to /articles/new.

Show a list of articles

First, let's see how to show all articles when a user opens the /articles URL path.

Serve the templated content

Open com/example/plugins/Routing.kt and add the following code to the get handler:

package com.example.plugins import com.example.models.* import io.ktor.server.application.* import io.ktor.server.freemarker.* import io.ktor.server.http.content.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.server.util.* fun Application.configureRouting() { routing { route("articles") { get { call.respond(FreeMarkerContent("index.ftl", mapOf("articles" to articles))) } } } }

The call.respond function accepts the FreeMarkerContent object that represents content to be sent to the client. In our case, the FreeMarkerContent constructor accepts two parameters:

  • template is a name of a template loaded by the FreeMarker plugin. The index.ftl file doesn't exist yet, and we'll create it in the next chapter.

  • model is a data model to be passed during template rendering. In our case, we pass an already created list of articles in the articles template variable.

Create a template

The FreeMarker plugin is configured to load templates located in the templates directory. First, create the templates directory inside resources. Then, create the index.ftl file inside resources/templates and fill it with the following content:

<#-- @ftlvariable name="articles" type="kotlin.collections.List<com.example.models.Article>" --> <!DOCTYPE html> <html lang="en"> <head> <title>Kotlin Journal</title> </head> <body style="text-align: center; font-family: sans-serif"> <img src="/static/ktor_logo.png"> <h1>Kotlin Ktor Journal </h1> <p><i>Powered by Ktor & Freemarker!</i></p> <hr> <#list articles?reverse as article> <div> <h3> <a href="/articles/${article.id}">${article.title}</a> </h3> <p> ${article.body} </p> </div> </#list> <hr> <p> <a href="/articles/new">Create article</a> </p> </body> </html>

Let's examine the main pieces of this code:

  • A comment with @ftlvariable declares a variable named articles with the List<Article> type. This comment helps IntelliJ IDEA resolve attributes exposed by the articles template variable.

  • The next part contains header elements of our journal - a logo and a heading.

  • Inside the list tag, we iterate through all the articles and show their content. Note that the article title is rendered as a link to a specific article (the /articles/${article.id} path). A page showing a specific article will be implemented later in Show a created article.

  • A link at the bottom leads to /articles/new for creating a new article.

At this point, you can already run the application and see the main page of our journal.

Refactor a template

Given that a logo and a heading should be displayed on all pages of our application, let's refactor index.ftl and extract common code to a separate template. This can be accomplished using FreeMarker macros. Create the resources/templates/_layout.ftl file and fill it with the following content:

<#macro header> <!DOCTYPE html> <html lang="en"> <head> <title>Kotlin Journal</title> </head> <body style="text-align: center; font-family: sans-serif"> <img src="/static/ktor_logo.png"> <h1>Kotlin Ktor Journal </h1> <p><i>Powered by Ktor & Freemarker!</i></p> <hr> <#nested> <a href="/">Back to the main page</a> </body> </html> </#macro>

Then, update the index.ftl file to reuse _layout.ftl:

<#-- @ftlvariable name="articles" type="kotlin.collections.List<com.example.models.Article>" --> <#import "_layout.ftl" as layout /> <@layout.header> <#list articles?reverse as article> <div> <h3> <a href="/articles/${article.id}">${article.title}</a> </h3> <p> ${article.body} </p> </div> </#list> <hr> <p> <a href="/articles/new">Create article</a> </p> </@layout.header>

Create a new article

Now let's handle requests for the /articles/new path. Open Routing.kt and add the following code inside get("new"):

get("new") { call.respond(FreeMarkerContent("new.ftl", model = null)) }

Here we respond with the new.ftl template without a data model since a new article doesn't exist yet.

Create the resources/templates/new.ftl file and insert the following content:

<#import "_layout.ftl" as layout /> <@layout.header> <div> <h3>Create article</h3> <form action="/articles" method="post"> <p> <input type="text" name="title"> </p> <p> <textarea name="body"></textarea> </p> <p> <input type="submit"> </p> </form> </div> </@layout.header>

The new.ftl template provides a form for submitting an article content. Given that this form sends data in a POST request to the /articles path, we need to implement a handler that reads form parameters and adds a new article to the storage. Go back to the Routing.kt file and add the following code in the post handler:

post { val formParameters = call.receiveParameters() val title = formParameters.getOrFail("title") val body = formParameters.getOrFail("body") val newEntry = Article.newEntry(title, body) articles.add(newEntry) call.respondRedirect("/articles/${newEntry.id}") }

The call.receiveParameters function is used to receive form parameters and get their values. After saving a new article, call.respondRedirect is called to redirect to a page showing this article. Note that a URL path for a specific article contains an ID parameter whose value should be obtained at runtime. We'll take a look at how to handle path parameters in the next chapter.

Show a created article

To show the content of a specific article, we'll use the article ID as a path parameter. In Routing.kt, add the following code inside get("{id}"):

get("{id}") { val id = call.parameters.getOrFail<Int>("id").toInt() call.respond(FreeMarkerContent("show.ftl", mapOf("article" to articles.find { it.id == id }))) }

call.parameters is used to obtain the article ID passed in a URL path. To show the article with this ID, we need to find this article in a storage and pass it in the article template variable.

Then, create the resources/templates/show.ftl template and fill it with the following code:

<#-- @ftlvariable name="article" type="com.example.models.Article" --> <#import "_layout.ftl" as layout /> <@layout.header> <div> <h3> ${article.title} </h3> <p> ${article.body} </p> <hr> <p> <a href="/articles/${article.id}/edit">Edit article</a> </p> </div> </@layout.header>

The /articles/${article.id}/edit link at the bottom of this page should open a form for editing or deleting this article.

Edit or delete an article

A route for editing an article should look as follows:

get("{id}/edit") { val id = call.parameters.getOrFail<Int>("id").toInt() call.respond(FreeMarkerContent("edit.ftl", mapOf("article" to articles.find { it.id == id }))) }

Similar to a route for showing an article, call.parameters is used to obtain the article identifier and find this article in a storage.

Now create resources/templates/edit.ftl and add the following code:

<#-- @ftlvariable name="article" type="com.example.models.Article" --> <#import "_layout.ftl" as layout /> <@layout.header> <div> <h3>Edit article</h3> <form action="/articles/${article.id}" method="post"> <p> <input type="text" name="title" value="${article.title}"> </p> <p> <textarea name="body">${article.body}</textarea> </p> <p> <input type="submit" name="_action" value="update"> </p> </form> </div> <div> <form action="/articles/${article.id}" method="post"> <p> <input type="submit" name="_action" value="delete"> </p> </form> </div> </@layout.header>

Given that HTML forms don't support PATCH and DELETE verbs, the page above contains two separate forms for editing and deleting an article. On the server side, we can distinguish POST requests sent by these forms by checking the input's name and value attributes.

Open the Routing.kt file and insert the following code inside post("{id}"):

post("{id}") { val id = call.parameters.getOrFail<Int>("id").toInt() val formParameters = call.receiveParameters() when (formParameters.getOrFail("_action")) { "update" -> { val index = articles.indexOf(articles.find { it.id == id }) val title = formParameters.getOrFail("title") val body = formParameters.getOrFail("body") articles[index].title = title articles[index].body = body call.respondRedirect("/articles/$id") } "delete" -> { articles.removeIf { it.id == id } call.respondRedirect("/articles") } } }

This code works as follows:

  • call.parameters is used to obtain the ID of the article to be edited.

  • call.receiveParameters is used to get the action initiated by a user - update or delete.

  • Depending on the action, the article is updated or deleted from the storage.

Run the application

Let's see if our journal application is performing as expected. We can run our application by pressing the Run button next to fun main(...) in our Application.kt:

Run Server

IntelliJ IDEA will start the application, and after a few seconds, we should see the confirmation that the app is running:

[main] INFO Application - Responding at http://0.0.0.0:8080

Open http://localhost:8080/ in a browser and try to create, edit, and delete articles:

Ktor journal

However, if you stop the server, all saved articles vanish as we are storing them in an in-memory storage. In the next tutorial, we'll show you how to add persistence to the website using the Exposed framework: Database persistence with Exposed.

Last modified: 25 November 2022