Quickly building a Kotlin REST Api Server using Ktor
What is this article about?
This article aims to be an introduction of how it’s possible to quickly build a REST API with Ktor.
Why?
I’m new to Ktor and it was hard to find good material to study about it. When you compare the amount of content of Spring, for example, probably you would say that we have almost no material about Ktor.
But, back to the first question: Why would anyone use Ktor if it is possible to use Spring with Kotlin?
font: KotlinConf 2018 – Comparing Kotlin Server Frameworks by Ken Yee
In the image above we can see that by far, Spring is the framework with more features. But features and popularity for sure are two important points when choosing a server framework, but that’s not everything. Support and documentation are also a very important topic, and Ktor being backed by JetBrains make both support and documentation really good and up to date.
The most important, KTOR uses all functionalities that Kotlin coroutines give us. (Kotlin coroutines is a whole another topic and you can check an amazing explanation in this video.) In a few words, coroutines makes Ktor really efficient.
How does Ktor work?
Before getting into code, first let’s understand how Ktor works, especially how it is composed.
font : KotlinConf 2018 – Building Server Backends with Ktor by Ryan Harter
The main actor in a Ktor server is the Application object. It is responsible for accepting requests from the servlet engine and return responses.
A Ktor web app has interchangeable engines, such as Tomcat, or as an embedded application, using Jetty, Netty or CIO. In this tutorial I am going to use Netty.
Another important part of a Ktor application are Features. Their function is pretty much to add functionalities to the server. For example, routing, serialization, and authentication.
Hands on
– Prerequisites: Download Intellij
Setting up
Ktor is really easy to set up and there are many ways to do this. You can choose between using Maven, Gradle, start.ktor.io and the IntelliJ Plugin. I followed the Gradle tutorial.
After the instructions, you will end-up with a structure of folders like this:
Hello World
The Application.kt file is the one that we are going to focus on this article. So make sure that your Application.kt looks like this:
package com.example | |
import io.ktor.application.* | |
import io.ktor.http.* | |
import io.ktor.response.* | |
import io.ktor.routing.* | |
import io.ktor.server.engine.* | |
import io.ktor.server.netty.* | |
fun main(args: Array<String>) { | |
embeddedServer(Netty, 8080) { | |
routing { | |
get("/") { | |
call.respondText("Hello, world!", ContentType.Text.Html) | |
} | |
} | |
}.start(wait = true) | |
} |
This is the simplest server that you can write using ktor. After starting the main function, if you go to http://localhost:8080/ in your browser, you will see “Hello Word”.
In the code we declared our server inside a routing, which is a feature that we are installing. It will be the main wrapper for your code, it is where all the requests will be handled.
But if something goes wrong? It would be nice to throw an exception, so let’s have a look on that.
Exceptions
To make this server useful we need more than static reponses. The majority of API need typed responses.
Let’s change the server code, in order to be able to have a Response class.
package com.example | |
import io.ktor.application.* | |
import io.ktor.http.* | |
import io.ktor.response.* | |
import io.ktor.routing.* | |
import io.ktor.server.engine.* | |
import io.ktor.server.netty.* | |
fun main(args: Array<String>) { | |
embeddedServer(Netty, 8080) { | |
routing { | |
get("/") { | |
call.respond(Response (status = "OK")) | |
} | |
} | |
}.start(wait = true) | |
} | |
data class Response (val status: String) |
Now, let’s do the request using curl, so appling the following to a terminal:
curl -X GET http://localhost:8080 |
We will receive nothing from the server. Wouldn’t it be nice if the server told us what the problem was?
In order to get the exceptions to the client we will install a Feature called StatusPage, we can check the standard feature installation notation bellow:
package com.example | |
import io.ktor.application.* | |
import io.ktor.features.StatusPages | |
import io.ktor.http.ContentType | |
import io.ktor.http.HttpStatusCode | |
import io.ktor.response.* | |
import io.ktor.routing.* | |
import io.ktor.server.engine.* | |
import io.ktor.server.netty.* | |
fun main(args: Array<String>) { | |
embeddedServer(Netty, 8080) { | |
install(StatusPages) { | |
exception<Throwable> { e -> | |
call.respondText(e.localizedMessage, ContentType.Text.Plain, HttpStatusCode.InternalServerError) | |
} | |
} | |
routing { | |
get("/") { | |
call.respond(Response (status = "OK")) | |
} | |
} | |
}.start(wait = true) | |
} | |
data class Response (val status: String) |
The StatusPage was configured to intercept Throwable which is any exception that might be thrown. We set the response to be the text of the error the response code to be internal server error. Running the request again using curl we will receive the error message as response
Json serialization
Next step is to install another feature, called ContentNegotiation. This is nothing new invented by Ktor, it’s just the standard mechanism by which a server and a client establish what type of content they are going to communicate with.
For that you can choose between many libraries, for example Jackson, Gson, kotlinx.serialization. I chose jackson, and now code is this:
import com.fasterxml.jackson.databind.SerializationFeature | |
import io.ktor.application.* | |
import io.ktor.features.ContentNegotiation | |
import io.ktor.features.StatusPages | |
import io.ktor.http.ContentType | |
import io.ktor.http.HttpStatusCode | |
import io.ktor.jackson.jackson | |
import io.ktor.response.respond | |
import io.ktor.response.respondText | |
import io.ktor.routing.get | |
import io.ktor.routing.routing | |
import io.ktor.server.engine.embeddedServer | |
import io.ktor.server.netty.Netty | |
fun main(args: Array<String>) { | |
embeddedServer(Netty, 8080) { | |
install(StatusPages) { | |
exception<Throwable> { e -> | |
call.respondText(e.localizedMessage, ContentType.Text.Plain, HttpStatusCode.InternalServerError) | |
} | |
} | |
install(ContentNegotiation) { | |
jackson { | |
enable(SerializationFeature.INDENT_OUTPUT) | |
} | |
} | |
routing { | |
get("/") { | |
call.respond(Response(status = "OK")) | |
} | |
} | |
}.start(wait = true) | |
} | |
data class Response(val status: String) |
If you apply the get request again you will receive an Json, representing the Response object.
Json deserialization
We’ve just sent a Json in the response. Now let’s receive Json in the request as well. And inside our routing there is the call object that serves as a wrapper for the response, the request and few other items that travel throughout the pipeline (please don’t worry about the details). But long story short, in the following piece of code you can check that now we have a post route and we are receiving the Request object inside, and responded this same object as well.
package com.example | |
import com.fasterxml.jackson.databind.SerializationFeature | |
import io.ktor.application.* | |
import io.ktor.features.ContentNegotiation | |
import io.ktor.features.StatusPages | |
import io.ktor.http.ContentType | |
import io.ktor.http.HttpStatusCode | |
import io.ktor.jackson.jackson | |
import io.ktor.request.receive | |
import io.ktor.response.respond | |
import io.ktor.response.respondText | |
import io.ktor.routing.get | |
import io.ktor.routing.post | |
import io.ktor.routing.routing | |
import io.ktor.server.engine.embeddedServer | |
import io.ktor.server.netty.Netty | |
fun main(args: Array<String>) { | |
embeddedServer(Netty, 8080) { | |
install(StatusPages) { | |
exception<Throwable> { e -> | |
call.respondText(e.localizedMessage, ContentType.Text.Plain, HttpStatusCode.InternalServerError) | |
} | |
} | |
install(ContentNegotiation) { | |
jackson { | |
enable(SerializationFeature.INDENT_OUTPUT) | |
} | |
} | |
routing { | |
get("/") { | |
call.respond(Response(status = "OK")) | |
} | |
post("/"){ | |
val request = call.receive<Request>() | |
call.respond(request) | |
} | |
} | |
}.start(wait = true) | |
} | |
data class Request (val id : String, | |
val quantity: Int, | |
val isTrue: Boolean | |
) | |
data class Response(val status: String) |
So now let’s build a request to test it. We need to set the a Content-Type to tell the server that we are sending a Json data.
curl -X POST http://localhost:8080 -H 'Content-Type: application/json' \ | |
-d '{ | |
"id" : "someId", | |
"quantity" : 10, | |
"isTrue" : false | |
}' |
If you send this request you will receive exact the same Json as response. It sounds simple. But if you take a closer look in what is happening, the server got the data deserialized automatically based in the Request class just calling the call.receive<Request>, and gave us an actual object. In the next line through call.respond(request) the server takes the object and automatically converted it to a Json returning to the client.
Finally, let’s do one last test. If you send a request that the server can not deserialize, for example, a string in the field quantity, what would happen?
The server will throw an exception, but do you remember that the StatusPage is configured to get any exception? Try it out, you should receive a response with the error message and a Http status code of Internal Server Error (500).
Reference :
- https://ktor.io/
- https://www.youtube.com/watch?v=V4PS3IjIzlw
- https://www.youtube.com/watch?v=8xfQA10Cd7g
- https://ryanharrison.co.uk/2019/06/30/using-ktor-with-jackson-json.html