Getting started with Ktor | Kotlin.
Tags: ktor, kotlin
Published: 2023-08-08 16:44:49

---
title: Getting started with Ktor | Kotlin.
published: true
date: 2023-08-08 02:28:31 UTC
tags: #ktor #kotlin 
canonical_url: https://carlosmv.hashnode.dev/getting-started-with-ktor-kotlin
---

In this article, we are going to learn how to build a server with the Ktor framework. This article will cover how to create a Ktor project using IntelliJ Ultimate and without it, build a basic server that returns a "Hello World" as a response, how to serve static files, and finally, an HTTP API that performs CRUD operations.

### Ktor

Ktor is an asynchronous framework for creating microservices, web applications and more. Written in Kotlin from the ground up. More about it here.

### Requirements

- Java SDK installed

- Gradle installed (Download it from [here](https://gradle.org/install/), install it and add it to the PATH environment)

### How to create a Ktor project with IntelliJ Ultimate

We open IntelliJ Ultimate, and select "New Project".

Then, we select "Ktor" in the left panel, write a name for the project, choose a "Location" and "Build system" if we want, or we can leave the default values, and click on "Next".

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1690656332504/bc1cf465-bd6d-44a0-9ebe-eb104c7195da.png)

We select the plugins we are going to use, for start this example, we will use "Routing" and "Call Logging". After we select the plugins, we click on "Create".

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1690601475450/9dbb8890-60e7-4471-8d06-d3c53d612087.png)

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1690601518311/75dd9316-d37a-4cfc-9e55-cdaea309af85.png)

IntelliJ creates the project and generates the plugins files we are going to use, and the `Application.kt` file.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1690656801086/5849e5e7-7eb3-41e7-958b-46792ab7a739.png)

We open the `Application.kt` file, and click on the "Play" button. There are two buttons, anyone works.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1690656948855/9a76d188-722c-4c03-a6fc-e07f60ed29f7.png)

The building process will start.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1690657022711/297b16c7-eb13-4133-8805-81008a8d7654.png)

Once is completed, we will have the app running.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1690657086250/85e01f4b-0355-4192-a05c-0bf0d9b52d6b.png)

If we navigate to `localhost:8080`, we should see the following message.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1690657132983/07929249-c6eb-4f7d-bcf3-e3600e4226bf.png)

### How to create a Ktor with another code editor.

To create a new Ktor project, we use the Ktor project generator on [this web page](https://start.ktor.io/#/settings?name=ktor-sample&website=example.com&artifact=com.example.ktor-sample&kotlinVersion=1.9.0&ktorVersion=2.3.2&buildSystem=GRADLE_KTS&engine=NETTY&configurationIn=CODE&addSampleCode=true&plugins=).

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1690654345670/348ecb46-31c9-4730-aff6-51f9b6c2b84d.png)

Then, we click on "Add plugins", and select "Routing" and "Call Logging".

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1690654467681/57356028-9f63-4626-8cfa-9791470e581a.png)

We click on "Generate project" and a .zip folder will be downloaded to our machine.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1690654540296/3a6d759f-0ea5-413b-a38a-ed106b385afb.png)

We unzip it and open the folder with a code editor. For this example, I will use VS Code.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1690654729122/f1e96423-c8db-412a-8d1c-3fed72249937.png)

We execute the `gradle run` command in the root folder.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1690655666422/d4696a11-9b75-459d-8d48-a4677d48cff8.png)

After the building is completed, we will see this output in the command line:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1690655890342/c6b78453-80d0-41ce-9416-5149961ac05a.png)

If we navigate to localhost:8080, we should see the following message:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1690655955712/c13de880-2875-4082-a287-6caca07e3ba5.png)

### Ktor server

**Src/main/kotlin Folder**

In the `src/main/kotlin` folder we have the `Application.kt` file and the plugins folder which contains the `Routing.kt` and `Monitoring.kt` files.

**Application.kt**

```
package com.example

import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import com.example.plugins.*

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

fun Application.module() {
    configureMonitoring()
    configureRouting()
}

```

The `Application.kt` file is the main entry point for a Ktor application.

The `main()` function in the `Application.kt` file is responsible for starting the Ktor server. The `module()` function is responsible for configuring the application and loading the plugins. In the code snippet above, `configureMonitoring()` install the plugin responsible for collecting the metrics of the app. Also, this file is where we define the port and host from where our application should start listening.

**Routing.kt**

```
package com.example.plugins

import io.ktor.server.routing.*
import io.ktor.server.response.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.http.*
import io.ktor.server.application.*

fun Application.configureRouting() {
    install(StatusPages) {
        exception<Throwable> { call, cause ->
            call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError)
        }
    }
    routing {
        get("/") {
            call.respondText("Hello World!")
        }
    }
}

```

The `routing()` function in the `Routing.kt` file is responsible for defining routes. The `routing()` function takes a block of code as its argument. The code in the block of code defines the routes for the application.

The routes are defined using the `get()`, `post()`, `put()`, `delete()`, and `head()` functions. These functions take a path as their argument and a handler as their return value.

The handler is the function that will be called when a request is made to the specified path. Inside the handler, we can get access to the ApplicationCall, which handles client requests and send the defined response.

In the code snippet above the `configureRouting()` function installs the StatusPages plugin and configures it to respond to all Throwable exceptions with a 500 Internal Server Error response.

The `routing()` function defines a route that responds to GET requests to the `/` path. Then, the handler for this route simply responds with the text "Hello World!".

The `StatusPages` plugin allows you to customize the way that Ktor responds to errors. The exception handler allows you to handle calls that result in a Throwable exception. In the code above it will throw the 500 HTTP status code for any exception.

Installing the StatusPages plugin in the `configureRouting()` function allows this plugin to be applied to all routes in the application.

For more information about the `Routing` and `StatusPages` plugins, you can consult the [documentation here](https://ktor.io/docs/routing-in-ktor.html).

### Serving Static Files

We are going to serve an HTML file. First, we create a folder in the root directory, called "static" with an HTML file in there.

index.html

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Index File</title>
</head>
<body>
    <h1>Hello, this is a HTML file</h1>

</body>
</html>

```

Then, we go to `Routing.kt` and add the following code in the `Application.configureRouting()` function.

```
routing {
        staticFiles("/static", File("static"), index="index.html")
...
}

```

This maps `/static` to the folder "static" and serve the `index.html` file as default.

```
package com.example.plugins

import io.ktor.server.routing.*
import io.ktor.server.response.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.http.content.*
import io.ktor.server.routing.get
import java.io.File

fun Application.configureRouting() {
    install(StatusPages) {
        exception<Throwable> { call, cause ->
            call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError)
        }
    }
    routing {
        staticFiles("/static", File("static"), index="index.html")

        get("/") {
            call.respondText("Hello World!")
        }
    }
}

```

In `Routing.kt`, we import `io.ktor.server.http.content.*` , `io.ktor.server.routing.get` , [`java.io`](http://java.io)`.File` and use the `staticFiles` function to define the route that serves the file, the folder and the file that is served as default.

We run the server and navigate to `localhost:8080/static`. We should receive the following response:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1691111167496/356a2cea-59fe-4f99-b652-827ac845d766.png)

Also, if we don't want to define a default file, we just keep the `staticFile` function like this:

```
routing {
        staticFiles("/static", File("static"))

```

As the documentation says, Ktor recursively serves up any file from `static` as long as the URL path and the filename match.

For example, if we have more than one file in the folder "static", and the filename of one of them is `hello.html`, to serve it, the URL path would be `/static/hello.html` .

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1691112782087/2adbcdcf-c541-4ae1-b480-d94ceac97c97.png)

## HTTP API

### Adding serialization plugin

For the HTTP API, we need to install the serialization plugin.

We open `build.gradle.kts` file and add the following dependency:

```
implementation("io.ktor:ktor-server-content-negotiation:$ktor_version")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")

```

Also, we have to declare the plugin in the same file.

```
plugins {
    id("org.jetbrains.kotlin.plugin.serialization") version "1.8.0"
}

```

Complete `build.gradle.kts`.

```
val ktor_version: String by project
val kotlin_version: String by project
val logback_version: String by project

plugins {
    kotlin("jvm") version "1.9.0"
    id("io.ktor.plugin") version "2.3.2"
}

group = "com.example"
version = "0.0.1"
application {
    mainClass.set("com.example.ApplicationKt")

    val isDevelopment: Boolean = project.ext.has("development")
    applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("io.ktor:ktor-server-core-jvm:$ktor_version")
    implementation("io.ktor:ktor-server-host-common-jvm:$ktor_version")
    implementation("io.ktor:ktor-server-status-pages-jvm:$ktor_version")
    implementation("io.ktor:ktor-server-call-logging-jvm:$ktor_version")
    implementation("io.ktor:ktor-server-netty-jvm:$ktor_version")
    implementation("ch.qos.logback:logback-classic:$logback_version")
    implementation("io.ktor:ktor-server-content-negotiation:$ktor_version")
    implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")
    testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
}

```

**Application.kt**

We have to declare a function inside the `Application.module()` function that loads the serialization plugin.

```
package com.example

import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import com.example.plugins.*

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

fun Application.module() {
    configureMonitoring()
    configureRouting()
    configureSerialization()
}

```

In the code above, we add `configureSerialization()`, which is responsible for loading the serialization plugin. But, we have not define that function yet.

Now, we create the `Serialization.kt` file inside the plugins folder.

**Serialization.kt**

```
package com.example.plugins

import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*

fun Application.configureSerialization() {
    install(ContentNegotiation) {
        json()
    }
}

```

In this file, we define the `configureSerialization()` function, responsible for installing the ContentNegotiation plugin.

**Users.kt**

We create the models package inside the `src/main/kotlin/com/example` folder.

```
package com.example.models

import kotlinx.serialization.Serializable

@Serializable
data class User(val id: String, val firstName: String, val lastName: String, val email: String)

```

In the code above, we import the `kotlinx.serialization.Serializable` library and, we define a data class called `User`. The `User` class has four properties: `id` , `firstName` , `lastName` , `email`.

The `@Serializable` annotation tells the Kotlin compiler to generate code that can be used to serialize and deserialize `User` objects.

For this example app, we are not going to use a database.

In the same file, we declare a mutable list of `User` objects called `userStorage`. The `mutableListOf` function creates a new list that can be modified.

```
val userStorage = mutableListOf<User>()

```

The `User` objects in the list can be added, removed, or changed at any time. This way we will be able to have CRUD functionality.

We create a package named `routes`, inside the `src/main/kotlin/com/example` folder.

Then, inside the `routes` package, we create the `UserRouter.kt` file.

**UserRouter.kt**

```
package com.example.routes

import com.example.models.*
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.userRouting() {
    route("/user") {
        get {

        }
        get("{id}") {

        }
        post {

        }
        put("{id}") {

        }
        delete("{id}") {

        }
    }
}

```

In this file, we declare the `userRouting` function, in which we group all the routes for the `/user` endpoint.

### GET route

```
package com.example.routes

import com.example.models.*
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.userRouting() {
    route("/user") {
        get {
            if (userStorage.isNotEmpty()) {
                call.respond(userStorage)
            } else {
                call.respondText ("No users found", status = HttpStatusCode.OK)
            }

        }
        ...
    }
}

```

Now, we added a handler for the `get` route to see if there are any users in the `userStorage` list. If there are no users in the list, the route will respond with a `200 OK` status code and the text "No users found".

Before we start the server to try this functionality, we have to go `Routing.kt` file and declare the `userRouting()` function inside the `configureRouting()` function.

**Routing.kt**

```

...
import com.example.routes.*

fun Application.configureRouting() {
    install(StatusPages) {
        exception<Throwable> { call, cause ->
            call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError)
        }
    }
    routing {
        staticFiles("/static", File("static"))

        get("/") {
            call.respondText("Hello World!")
        }
        userRouting()
    }
}

```

Now we can start the server and navigate to `localhost:8080/user`. We should receive the following response:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1691195314019/1f1b9861-a331-45b2-9b23-3ea297c30c60.png)

We received that message because so far there is no data in the `userStorage`.

Let's add a handler to create a user.

### POST route

```
 post {
    val customer = call.receive<User>()
    customerStorage.add(user)
    call.respondText("User stored correctly", status = HttpStatusCode.Created)

 }

```

We run the server. And use an HTTP client to make a POST request.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1691196237026/68a9f678-5089-4e6c-be6a-6012cb3ace9f.png)

If we navigate to `localhost:8080/user` or make a GET request with an HTTP client, we should see the following response.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1691198053903/2c88d76d-22d3-46a5-9e30-4edccd758a72.png)

### GET ID route

```
get("{id}") {
  val id = call.parameters["id"] ?: return@get call.respondText(
       "Missing id",
       status = HttpStatusCode.BadRequest
       )
 val user =
     userStorage.find { it.id == id } ?: return@get call.respondText(
     "No user with id $id",
      status = HttpStatusCode.NotFound
      )
 call.respond(user)
}

```

This handler retrieves the user with the specified ID from the `userStorage` list and returns it to the client. If the user does not exist, the handler returns a `404 Not Found` error.

The handler first uses the `call.parameters` function to get the value of the `id` parameter from the request. If the `id` parameter is not present, the handler returns a `400 Bad Request` error.

Next, the handler uses the `userStorage.find` function to find the user with the specified ID in the `userStorage` list. If the user is not found, the handler returns a `404 Not Found` error.

If the user is found, the handler returns the user to the client. The `respond` function sends a response to the client.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1691199198964/1a60cf82-532b-4d72-8f7d-574aea8cadde.png)

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1691199223310/55a9c7f3-2dd9-4784-95f4-4d9c6a68a567.png)

### PUT route

```
put("{id}") {
 val id = call.parameters["id"] ?: return@put call.respondText(
     "No id provided",
     status = HttpStatusCode.BadRequest
     )
 val user =
     userStorage.find { it.id == id } ?: return@put call.respondText(
      "No user with this id: $id",
       status = HttpStatusCode.NotFound
      )
 val newData = call.receive<User>()
 val indexUser = userStorage.indexOf(user)
 userStorage[indexUser] = newData
 call.respondText("User updated", status = HttpStatusCode.OK)      
}

```

Now, we implement the option that allows the clients to updates an element in the `userStorage`. This handler updates the user with the specified ID with the data from the request body. If the user does not exist, the handler returns a `404 Not Found` error.

The handler first uses the `call.parameters` function to get the value of the `id` parameter from the request. If the `id` parameter is not present, the handler returns a `400 Bad Request` error.

Next, the handler uses the `userStorage.find` function to find the user with the specified ID in the `userStorage` list. If the user is not found, the handler returns a `404 Not Found` error.

If the user is found, the handler uses the `call.receive<User>` function to read the data from the request body and create a new `User` object. The new `User` object is then used to update the existing user in the `userStorage` list. The index of the existing user in the list is used to ensure that the correct user is updated.

Finally, the handler returns a `200 OK` status code and the text "User updated".

### DELETE route

```
delete("{id}") {
  val id = call.parameters["id"] ?: return@delete call.respond(HttpStatusCode.BadRequest)
  if (userStorage.removeIf{ it.id == id}) {
      call.respondText("User removed correctly", status = HttpStatusCode.Accepted)

   } else {
       call.respondText("User Not Found", status = HttpStatusCode.NotFound)
   }
}

```

In the DELETE route, with a specified ID, this handler deletes a user from the `userStorage` list. If the user does not exist, the handler returns a `404 Not Found` error.

Similar to the GET and PUT handlers, the delete handler first uses the `call.parameters` function to get the value of the `id` parameter from the request. If the `id` parameter is not present, the handler returns a `400 Bad Request` error.

Next, the handler uses the `userStorage.removeIf` function to remove the user with the specified ID from the `userStorage` list. If the user is not found, the handler returns a `404 Not Found` error.

Finally, the handler returns a `202 Accepted` status code if the user was deleted successfully, or a `404 Not Found` status code if the user was not found.

### Monitoring Plugin

When we create this project, we select the Call logging plugin. With this plugin, we can see in the console every request made to the server.

```
package com.example.plugins

import io.ktor.server.plugins.callloging.*
import org.slf4j.event.*
import io.ktor.server.request.*
import io.ktor.server.application.*

fun Application.configureMonitoring() {
    install(CallLogging) {
        level = Level.INFO
        filter { call -> call.request.path().startsWith("/") }
    }
}

```

The `CallLogging` plugin logs information about every incoming request, including the request method, path, headers, and body. The `configureMonitoring` function configures the `CallLogging` plugin with the following settings:

- The log level is set to `INFO`. This means that only informational messages will be logged.

- The filter function is used to only log requests that start with the "/" path.

## Conclusion

In this article, we have learned how to build a server with Ktor that serves static files and performs CRUD operations through an HTTP API. We started by creating a new project and adding plugins. We also learned how to use Ktor's routing features to create an endpoint.

In my opinion, Ktor has amazing documentation. I don't have any experience using Kotlin, but it was easy to learn, and it allows me to learn quickly how to build a server.

Thank you for taking the time to read this article.

If you have any recommendations about other packages, architectures, how to improve my code, my English, or anything; please leave a comment or contact me through [**Twitter**](https://twitter.com/Carlos_marcv), or [**LinkedIn**](https://www.linkedin.com/in/carlos-marcano-a2135a134/).

The source code is [here](https://github.com/carlosm27/ktor-demo).

## Resources

[Creating HTTP APIs](https://ktor.io/docs/creating-http-apis.html)

[Creating a new Ktor project](https://ktor.io/docs/intellij-idea.html)

[Routing](https://ktor.io/docs/routing-in-ktor.html)

[Content negotiation and serialization](https://ktor.io/docs/serialization.html)

[Call logging](https://ktor.io/docs/call-logging.html)

[Ktor README](https://github.com/ktorio/ktor)