Kotlin backend with Ktor : 101

Kotlin backend with Ktor : 101

The 101 of Ktor to get started in building your own APIs

This is the second part of the series, Kotlin for backend. If you have not checked out the first part click here.

Since we understand that we can use Kotlin to build scalable backend apps. Let us move to the next step. We need to decide the framework we are going to use.

From the title, you might have known the answer to this, but let me still mention that we are going to use Ktor Framework by Jetbrains itself to build our backend services.

We also have different options like Vertx, Micronauts, etc I chose to go with Ktor because:

  1. It is built by Jetbrains, the same firm which has given us Kotlin.
  2. It is very lightweight.
  3. It supports Coroutines by default which helps the app to scale and it also supports all the features of Kotlin like DSLs.

In this blog, we are going to learn and understand the basics of API development using Kotlin and Ktor. We are going to divide the blog in:

  • Setting up the project
  • Running the starter project.
  • Building our own APIs.
  • and much more.

So, before any further due, let's start.

Setting up the project.

We can start setting up the project in two ways either follow this link Ktor Startup project generator or install the Ktor plugin in your Intellij IDE to start setting up your project. I will be following the Plugin way to create my project. Let's start, First I will create a new project using New project

Init project

Now, after going to next you will see the following screen:

My Startup

Here, first, we select Ktor from the left panel and we can only see this option if we have installed the Ktor Plugin mentioned above.

Once we have selected Ktor as an option, we need to fill up few details, like:

  • Name: It is used to define the name for the project
  • Location: It is used to specify the path in the local PC/Mac where we want to store the project.
  • Build System: It means the build system responsible to build the project. In our case, we will use Gradle Kotlin.
  • Website: This is the website we will use to generate project Artifact.
  • Artifact: This is only read-only generated via the Website you entered.
  • Ktor Version: This is used to specify the version we will use for Ktor. In our case, we will use 1.6.0, the latest version.
  • Engine: This is one of the most important parts of creating the project. This will be used to run the server. We will select Netty
  • Configuration in: This is used to say, that where do we need to store server variables.
  • Add Sample code: This will be used to add some dummy code to the project.

Now, after specifying all the things required for creating the project we press next.

Selection of Libs

In the above screen, we will select the features we need to finish the setup of the project.

For now, we will select Routing and click Finish to go to the project.

Note: We can also add other plugins later as per the need for our project. We are going to see a lot of them in coming blogs.

We have successfully created our project.

Running the starter project.

Project Structure

In the above image, you will see inside the src folder we have main and test folders. The main folder will contain all the code for the project and the test folder will have the test for your code.

Now, let's run the project and see the output. You can run the project by click run on IntelliJ. You will see the following as output:

Output And, when you open the URL, you will see "Hello World" as output like this:

output

So, this is the default route we will handle in the project.

Let's discuss the routing.

Routing is the plugin that we selected and is always installed by default. This is responsible for handling all the routes we will build for our project.

Initially, when you opened https://0.0.0.0:8080, it triggered the / route which displayed the Hello world result in the browser.

The routing is present inside, plugins -> Routing and it looks like:

fun Application.configureRouting() {
    routing {
        get("/") {
            call.respondText("Hello World!")
        }
    }
}

If you can see, here, we are using the extension function property and attaching configureRouting to the application. Inside that, we call the routing function and that handles the route.

Here, you can see, we are using GET to build a route to match a GET Request with an empty path.

And to register all the routes in the application we navigate to the Application file and inside the main function will pass configureRouting like:

fun main() {
    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
        configureRouting()
    }.start(wait = true)
}

Since configureRouting is an extension function of the Application we can directly call configureRouting here directly.

Building our own APIs.

In this section, we are going to see how we can build our own custom routes. We will build a custom Users route where we will build a couple of routes. They are:

  • GET all Users
  • GET an individual User
  • DELETE User
  • Create a User using POST.

First, we will create a model for the User which will have an ID, Name, and Age.

data class User(val id: Int, val name: String, val age: Int)

In this case, we won't be using a Database. To simulate the database in our project we will use a list(MutableList) type structure which will be responsible for adding/deleting users.

So, let me create a mutable list of user like:

val users = mutableListOf<User>()

Now, just to test let's update our default GET call like:

 get("/") {
     call.respond(users)
 }

And if you run this, you will get an error,

Response pipeline couldn't transform '*class java.util.ArrayList*' to the OutgoingContent

The reason is that the project can't serialize the list as Output.

Now, let's fix it.

To fix it we need to install Content Negotiation. To do this we will go to our Routing file and install it like:

 install(ContentNegotiation) {
   gson()
 }

To install Gson in the project, we will add its dependency in the Gradle file like:

    implementation("io.ktor:ktor-gson:$ktor_version")

Here, Content Negotiation will use Gson for Serializing/deserializing the content in the specific format.

And, finally, if you run the project and open http://0.0.0.0:8080/ you will see the following output.

Empty Users

Creating a User

In this section, we will see how we can create a user by adding the User JSON in the body of the request.

First, we will create a user route with POST request like:

 post("/user") {

 }

Now, as a next step, we will have to accept a body from a user. In Ktor, we use receive to take the request like:

 post("/user") {
    val requestBody = call.receive<User>()
 }

Now, the requestBody will be mapped to the User object and it will contain the JSON. We also need to add an ID to it.

What we would do here is, we will add the (index+1) as the ID every time we add an object to the list like:

 post("/user") {
            val requestBody = call.receive<User>()
            val user = requestBody.copy(id = users.size.plus(1))
            users.add(user)
  }

And, at last, we will respond the added user as a response. Finally, the route looks like this:

post("/user") {
            val requestBody = call.receive<User>()
            val user = requestBody.copy(id = users.size.plus(1))
            users.add(user)
            call.respond(user)
 }

Let's see this in action. We will first run the app and let us, open postman. We will fire up a request with User JSON and the response will also look like this:

Request-Response

Here, we passed on the request in the body and we got the response after adding it to the users list.

And finally, we will update the GET route to:

 get("/users") {
   call.respond(users)
 }

Get a specific User

In this section, we will get a specific user from the list by its id. It will be a GET request.

First, we need to create the route. We will name it as /user as well which will take id as a query parameter.

get("/user") {

}

In this block, we will get the id from the query like:

get("/user") {
            val id = call.request.queryParameters["id"]

 }

Now, we need to find the user from the user list matching the id. For this, we will use the find operator present in Kotlin like:

get("/user") {
            val id = call.request.queryParameters["id"]
            val user = users.find { it.id == id?.toInt() }
 }

Lastly, if the user is null, that means the user is not present and if it's not null we get the specific user. Let's convert that into code.

get("/user") {
            val id = call.request.queryParameters["id"]
            val user = users.find { it.id == id?.toInt() }
            val response = user ?: "User not found"
            call.respond(response)
}

For this to work, when you re-run the project, first you need to add a user and then find the user with an id.

Deleting a User.

In this section, we are going to delete a user if the provided userId is present. It will be pretty similar to what we did in getting user by id. The only change we are going to do is we need to replace GET to DELETE.

delete("/user") {
            val id = call.request.queryParameters["id"]
}

Now, we will just remove the user from the list if the user is present.

We will update the code like:

 delete("/user") {
            val id = call.request.queryParameters["id"]
            users.removeIf {
                it.id == id?.toInt()
            }
            call.respond("User deleted")
}

This will delete the user.

We have finally created all the routes we wanted to do at the start of the blog. This is the basic example to get started with creating routes in Ktor.

Finally, the code in my route looks like this:

data class User(val id: Int, val name: String, val age: Int)

val users = mutableListOf<User>()

fun Application.configureRouting() {
    install(ContentNegotiation) {
        gson()
    }

    routing {
        get("/users") {
            call.respond(users)
        }
        post("/user") {
            val requestBody = call.receive<User>()
            val user = requestBody.copy(id = users.size.plus(1))
            users.add(user)
            call.respond(user)
        }

        get("/user") {
            val id = call.request.queryParameters["id"]
            val user = users.find { it.id == id?.toInt() }
            val response = user ?: "User not found"
            call.respond(response)
        }

        delete("/user") {
            val id = call.request.queryParameters["id"]
            users.removeIf {
                it.id == id?.toInt()
            }
            call.respond("User deleted")
        }
    }
}

Summary

This is a very beginner-friendly starting point to get started to build your routes in Ktor. Go ahead and build your own. In the upcoming blogs, we will see advanced routing and diving deeper into understanding routes in Ktor. We will also see the grouping of routes in Ktor as well.

Say hi @hi_man_shoe

Did you find this article valuable?

Support Himanshu Singh by becoming a sponsor. Any amount is appreciated!