You already know how to get the data sent by the client. But your server must be able not only to receive the data but also to return a response. In this topic, we'll look at how to respond to client requests correctly.
Plain text
Let's start by displaying "Hello, user!" to the user. To output text, we use the function call.respondText, which you are probably familiar with.
get("/plaintext") {
call.respondText("Hello, user!")
}In the browser we see the response:
HTML
Passing text to the user is easy, but what if you want to return an entire HTML page?
When the Web first appeared, it was a set of static HTML pages connected by hyperlinks. But modern sites are more complex. For example, you want each user to get a greeting containing their name. Here, you can't make do with static HTML files. You have to generate your HTML page for each user.
To provide for the generation of HTML pages, there are so-called templating tools. You put your HTML code into a separate file called a template. And then you generate HTML pages based on that template.
There are several templating tools in the Java world. The most popular is FreeMarker. We will now demonstrate how to use it.
First you need to install the FreeMarker plugin. To do this, specify
implementation("io.ktor:ktor-server-freemarker:$ktor_version")in the build.gradle.kts file. Then add the install(FreeMarker) command to the embeddedServer function:
install(FreeMarker) {
templateLoader = ClassTemplateLoader(this::class.java.classLoader, "templates")
}(You have already read about installing plugins in Plugins system topic)
Next, create a template file resources/templates/html.ftl with the following contents:
<html>
<body>
<h2>Hello, ${userName}</h2>
</body>
</html>As you have already guessed, the ${userName} construction is used here to substitute the username in the template.
Now to return the HTML page generated from this template to the user we use the call.respondTemplate function:
get("/html") {
call.respondTemplate("html.ftl", mapOf("userName" to "Bob"))
}Result:
<html>
<body>
<h2>Hello, Bob</h2>
</body>
</html>As you can see, the variable substitution in the template is made using the map data structure. If you want to return the page to another user, just pass his name to the template: call.respondTemplate("html.ftl", mapOf("userName" to "UserName"). FreeMarker will substitute this name in your template and return the corresponding HTML to the user.
More information about FreeMarker in Ktor can be found in the official manual. We will talk about working with HTML in more detail in future topics.
JSON
If you want to send a user some object that you have stored on the server, you need to convert it into JSON first.
Serialization and deserialization in kotlin are done with the kotlinx library. Let's connect it by putting the appropriate lines in the build.gradle.kts file:
plugins {
kotlin("plugin.serialization") version "1.8.22"
}dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
}Next, you need to place the @Serializable annotation over the dataclass you want to translate to JSON:
import kotlinx.serialization.Serializable
@Serializable
data class User(val name: String, val lastname: String, val age: Int)After these preparatory steps, you can turn your objects into JSON with the function Json.encodeToString and return the resulting string as plain text:
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToStringpost("/page") {
val user = User("Bob", "Percival", 28)
call.respondText(Json.encodeToString(user))
}Now when you access /page, you will get JSON in response:
Return files
You probably already know how to store files as static content. But what if you want to return a file directly in the routing handler?
Ktor has a handy function for this purpose: call.respondFile
get("/file") {
val file = File("myfile.zip")
call.respondFile(file)
}Now if you put the myfile.zip in the root folder of your project, this file will be output to the user when localhost:8080/file is accessed.
Tip: You can create a /files folder in the root folder of the project and store files there. In that case, our file should be accessed as File("files/myfile.zip").
Status code
The response code is sent with the main body and allows the client to determine the status of the request without analyzing the content of the response itself.
For example, a 404 code means that the resource requested by the user was not found. Code 500 shows a server error. Code 200 means successful execution of the request. You can read the full list in MDN.
To set the response code in Ktor, use the call.response.status function.
get("/text") {
call.response.status(HttpStatusCode(500, "Server error"))
}When you open this page in your browser, you will see a server response code:
Headers and cookies
Ktor provides a simple API to set headers and response cookies.
The call.response.header function is used to set the response headers.
call.response.header("HeaderName", "HeaderValue")For example, let's add a Content-Disposition header to the previous file example.
By default, the browser opens some file types within a tab instead of downloading them. This is the case with pictures, for example. To make the browser always download files regardless of their type, set the header Content-Disposition: attachment.
get("/file") {
call.response.header("Content-Disposition", "attachment")
val file = File("image.png")
call.respondFile(file)
}Now when /file is accessed, the browser will download the picture to the user's computer instead of displaying it in a tab.
There is a similar function for setting cookies:
call.response.cookies.append("CookieName", "CookieValue")Cookies allow you to save information on the client's side and read it back the next time a request is made. In this way, sites can remember the state between multiple requests. (From the previous topic you should already know how to read cookies)
For example, let's make a clicker page that will increment the counter every time we access it. To do this, we will read the counter value from cookies, output it as plain text, and then increment the counter by one and store the new value in cookies.
get("/clicker") {
var count: Int
if(call.request.cookies["count"] != null)
count = call.request.cookies["count"]!!.toInt()
else
count = 0
count++
call.response.cookies.append("count", count.toString())
call.respondText(count.toString())
}Now every time you access /clicker the counter will increase by one.
Content-Type
There is also an important Content-Type header that tells the browser what format information you are sending in the response. For example, the text has the type text/plain, the image has the type image/png, the HTML code has the type text/html. You can find a complete list of types in MDN.
Ktor creates and sends the Content-Type header itself. We can't specify it explicitly with the call.response.header function, but some functions allow us to specify it by passing an additional parameter. For example, the call.respondText function sets the default Content-Type to text/plain, but we can forcibly change it to image/png:
get("/text") {
call.respondText("Hello, User!", ContentType.Image.PNG)
}If you now refer to the /text page, you notice that the browser will try to interpret your text as a picture.
Redirect
Sometimes it is necessary to redirect a user from one page to another.
For example, you have a page /greeting, where the message "Hello, user!" is displayed. But you want the user to access this page not only through /greeting, but also through /greet.
We can, of course, just implement the welcome message on the /greet page, but that would be quite useless, because if you want to change the welcome message, you will have to change two places. The most reasonable solution in this case would be to make a redirect from /greet to /greeting, so that you don't have to duplicate the code.
Redirects in Ktor are made very easy with the call.respondRedirect function:
get("/greet") {
call.respondRedirect("/greeting")
}
get("/greeting") {
call.respondText("Hello, user!")
}When /greet is accessed, the server will send redirection information, the user will be redirected to /greeting and will see the desired message.
Conclusion
In this topic, we've looked at different methods of sending a response to a user's request.
You know how to return plain text, HTML or even a file to the user. You also know how to configure response code, headers, and cookies. In addition, you have been introduced to the FreeMarker templating engine, which allows you to separate the HTML code for output from the application code.
Now it's time to put what you've learned into practice.