Ktor server for beginners – sending and receiving data

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.

5 2 votes
Article Rating
Subscribe
Notify of
guest

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments

[…] the previous lesson we made our first GET and POST requests and we used a mutable list of fruits to simulate adding and […]