Level: Beginner
Ktor is a lightweight and high-performance framework for building asynchronous servers and clients in Kotlin. It provides an easy-to-use and intuitive API for creating web applications, microservices, and other types of network applications. With its simple and concise syntax, Ktor allows developers to quickly create and deploy robust and scalable applications with minimal boilerplate code. In this blog post, we’ll explore the basics of Ktor server and learn how to send and receive data using its powerful and flexible features. Whether you’re a seasoned developer or just getting started with Kotlin and Ktor, this post will provide you with a solid foundation for building fast and reliable network applications.
Prerequisites
- Basic knowledge of Kotlin
- Know what Coroutines are
- Know what a Restful API is
- Basic knowledge of using Postman (to test our API)
Setup
To create our project we will use IntelliJ IDEA, you can use the Ultimate or Community version, for the Ultimate version you just create a new project and select Ktor
for the community version you need to go to: start.ktor.io, Pick a name for your project click add plugins and add the following:
- Routing (for basic routing functionality)
- Default Headers (to provide default headers for all responses)
- Call Logging (to show useful log information in intellij)
- Content Negotiation (to define the rules to negotiate content between server and client)
- kolinx.serialization (to convert our Kotlin data class from / to Json)
now click “Generate Project” download and extract your project in intellij.
Project structure
If we take a look at the files in src > main > kotlin > com > example we should find a file called “Application.kt” this file includes the main function witch is the starting point for our Kotlin project, in the same file we’ll find a function called “module” that extends “Application” this function includes the configuration to all plugins we added when we setup our project, feel free to explore these functions (left click + CTRL), take a look at “configureRouting“
this functions opens a routing block in side that routing block it opens a get block with “call.respondeText” inside, notice how call.respondText is suspending and we’re not getting any errors that’s because the get block is suspending and a coroutine is provided by default.
Run your server on intelliJ, Open postman and make a get request to “localhost:8080“, you should get a successful response with code 200 and a body of “Hello World!”.
Making our first endpoint
in the main directory for your kotlin files create a new package and call it “data” inside data create another package and call it “model” there create a new kotlin class named “SimpleResponse” with a boolean and a string
import kotlinx.serialization.Serializable
@Serializable
data class SimpleResponse(
val successful: Boolean,
val message: String
)
in the main directory for your kotlin files create a new package and call it “routes”, this package will include all of our routes. create your first route here by making a Kotlin file named SimpleRoute, inside this file create a new function getSimpleResponse that extends Route.
import io.ktor.server.routing.*
fun Route.getSimpleResponse(){
}
inside this function define a new route block and give it an end point for example “/get-simple”, inside the route block open a get block to define a Get request for this route, now inside the get block use call.respond to respond to calls to this end point and pass an object of the simple response data class we created earlier for example:
import com.example.data.model.SimpleResponse
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Route.getSimpleResponse(){
route("/get-simple"){
get {
val simpleResponse = SimpleResponse(successful = true, message = "This is a simple response")
// respond to the get request with a status code OK and a simple response
call.respond(HttpStatusCode.OK,simpleResponse)
}
}
}
Before we try our new end point in postman we need to define it in “configureRouting” function, open the “plugins” package > Routing.kt and add the “getSimpleResponse” function there
import com.example.routes.getSimpleResponse
import io.ktor.server.routing.*
import io.ktor.server.response.*
import io.ktor.server.application.*
fun Application.configureRouting() {
routing {
// add our new route
getSimpleResponse()
get("/") {
call.respondText("Hello World!")
}
}
}
now re-run the server and send a get request to our newly created end point 😀
Receiving parameters
Now that we send data from our server successfully lets receive data!, before we do that lets create a data class that represents a Fruit in the “model” package, then lets create a new route in a new file lets call it “FruitRoute” as we learned above create an end point, but this time returns a list, also don’t forget to add the function to Routing.kt file.
import kotlinx.serialization.Serializable
@Serializable
data class Fruit(
val name: String,
val id: String
)
import com.example.data.model.Fruit
import com.example.data.model.SimpleResponse
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Route.fruitRoute() {
// create a mutable list of fruits
val fruits = mutableListOf(
Fruit("apple", "1"),
Fruit("orange", "2"),
Fruit("banana", "3"),
Fruit("pineapple", "4"),
Fruit("tomato", "5"),
)
get("/fruits") {
// define the status code we would like to return, and return the list which will be converted automatically to Json
call.respond(HttpStatusCode.OK, fruits)
}
}
feel free to test this new end point, modifiy the get reqeust to take a parameter ‘id’ and return the fruit of that id when passed
import com.example.data.model.Fruit
import com.example.data.model.SimpleResponse
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Route.fruitRoute() {
// create a mutable list of fruits
val fruits = mutableListOf(
Fruit("apple", "1"),
Fruit("orange", "2"),
Fruit("banana", "3"),
Fruit("pineapple", "4"),
Fruit("tomato", "5"),
)
get("/fruits/{id?}") {
// get the id from the parameters if no id is passed return the full list
val id = call.parameters["id"] ?: return@get call.respond(HttpStatusCode.OK, fruits)
// define the status code we would like to return, and return the list which will be converted automatically to Json
call.respond(HttpStatusCode.OK, fruits.filter { it.id == id })
}
}
try to make a request with and without the ‘id’ parameter
Receiving body
to receive a body lets create a post request, this request will add an item to the fruit list.
import com.example.data.model.Fruit
import com.example.data.model.SimpleResponse
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Route.fruitRoute() {
// create a mutable list of fruits
val fruits = mutableListOf(
Fruit("apple", "1"),
Fruit("orange", "2"),
Fruit("banana", "3"),
Fruit("pineapple", "4"),
Fruit("tomato", "5"),
)
get("/fruits/{id?}") {
// get the id from the parameters if no id is passed return the full list
val id = call.parameters["id"] ?: return@get call.respond(HttpStatusCode.OK, fruits)
// define the status code we would like to return, and return the list which will be converted automatically to Json
call.respond(HttpStatusCode.OK, fruits.filter { it.id == id })
}
post("/add-fruit") {
// receive the fruit from the user
val newFruit = call.receive<Fruit>()
// add the received fruit to the list
fruits.add(newFruit)
// acknowledge that we successfully added the fruit by responding
call.respond(
HttpStatusCode.OK, SimpleResponse(
true,
"Successfully added ${newFruit.name}"
)
)
}
}
in post man change the request type to Post and then go to body, change type to JSON and type the body as a JSON like the picture below
as a last step lets respond with a bad request if the client doesn’t send a correct Fruit format, we do that by adding a try catch block like below
post("/add-fruit") {
try {
// receive the fruit from the user
val newFruit = call.receive<Fruit>()
// add the received fruit to the list
fruits.add(newFruit)
// acknowledge that we successfully added the fruit by responding
call.respond(
HttpStatusCode.Created, SimpleResponse(
true,
"Successfully added ${newFruit.name}"
)
)
}catch (ex: Exception){
call.respond(HttpStatusCode.BadRequest, SimpleResponse(false,"Invalid Fruit format"))
}
}
Finally the final FruitRoutes file should look like this:
import com.example.data.model.Fruit
import com.example.data.model.SimpleResponse
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Route.fruitRoute() {
// create a mutable list of fruits
val fruits = mutableListOf(
Fruit("apple", "1"),
Fruit("orange", "2"),
Fruit("banana", "3"),
Fruit("pineapple", "4"),
Fruit("tomato", "5"),
)
get("/fruits/{id?}") {
// get the id from the parameters if no id is passed return the full list
val id = call.parameters["id"] ?: return@get call.respond(HttpStatusCode.OK, fruits)
// define the status code we would like to return, and return the list which will be converted automatically to Json
call.respond(HttpStatusCode.OK, fruits.filter { it.id == id })
}
post("/add-fruit") {
try {
// receive the fruit from the user
val newFruit = call.receive<Fruit>()
// add the received fruit to the list
fruits.add(newFruit)
// acknowledge that we successfully added the fruit by responding
call.respond(
HttpStatusCode.Created, SimpleResponse(
true,
"Successfully added ${newFruit.name}"
)
)
}catch (ex: Exception){
call.respond(HttpStatusCode.BadRequest, SimpleResponse(false,"Invalid Fruit format"))
}
}
}
As always the project is available on Github.
[…] the previous lesson we made our first GET and POST requests and we used a mutable list of fruits to simulate adding and […]
Great job Mr. Taha