Ktor Web API – Endpoint Routing

Starting point – Initial Web API

We will start to create an initial Ktor project. Tutorial

Serve static content

Content negotiation and serialization plugins

The web API will transfer and receive objects in JSON format. In order to communicate and receive these objects we must first ensure that the client and server agree about the content. This step is called content negotiation. Basically, it will be decided on which format and standard is used to send data (in our JSON).

The second part is to configure the application how to serialize and deserialize objects from and to JSON. Kotlin has built-in support for serializing objects. We will be using the Serialization annotation.

First, we must adapt our build.gradle.kts file.

plugins {
    kotlin("jvm") version "1.8.0"
    id("io.ktor.plugin") version "2.2.3"
    id("org.jetbrains.kotlin.plugin.serialization") version "1.8.0"
}
...
dependencies {
    implementation("io.ktor:ktor-server-core-jvm:$ktor_version")
    implementation("io.ktor:ktor-server-content-negotiation:$ktor_version")
    implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")
    implementation("io.ktor:ktor-server-netty-jvm:$ktor_version")
    implementation("ch.qos.logback:logback-classic:$logback_version")
    testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
}

Once you have changed your gradle file, make sure to run the gradle build task to have all dependencies installed.

Now we must adapt our Application.kt

import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import plugins.configureRouting
import plugins.contentNegotiation

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

@Suppress("unused")
fun Application.module() {
    configureRouting()
    contentNegotiation()
}

In the Application module function, we will call a new extension function called contentNegotiation. This function is implemented in the file Serialization.kt. It defines to use of JSON as content media and adds some modifiers to how the JSON is sent. Note that in production code you want to have the most compact JSON possible.

package plugins

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

fun Application.contentNegotiation() {
    install(ContentNegotiation) {
        json(Json {
            prettyPrint = true
            isLenient = true
        })
    }
}

The current folder structure can be seen in the following image. You should try to build and run the current state of your application.

Ktor Web API folder structure with serialization and content negotiation plugin
Folder Structure with Serialization Plugin

Add and configure Endpoints

In the next step, we will add special API endpoints for 2 resources: Orders and Restaurants. These resources are fictive, but in a later step, we will add actual objects to these resources.

The code will be structured in such a way, that endpoints related to one resource are grouped together in one file. These files will be put in extra packages. This allows us to easily add new endpoint versions. Each of the files will add an extension function to the routing plugin.

package plugins

import io.ktor.server.routing.*
import io.ktor.server.application.*
import routing.orders.*
import routing.restaurants.*


fun Application.configureRouting() {
    routing {
        addOrderRoutes()
        addRestaurantRoutes()
    }
}

For each of the main route we will add endpoints for GET, POST, PUT and DELETE.

package routing.orders

import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun Route.addOrderRoutes () {

    route("api/v1/orders") {
        get() {
            call.respondText { "GET api/v1/orders" }
        }

        get("{id}") {
            call.respondText { "GET api/v1/orders/:id" }
        }

        post() {
            call.respondText { "POST api/v1/orders/:id" }
        }

        put("{id}") {
            call.respondText { "PUT api/v1/orders/:id" }
        }

        delete("{id}") {
            call.respondText { "DELETE api/v1/orders/:id" }
        }
    }
}
package routing.restaurants

import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun Route.addRestaurantRoutes () {
    route("api/v1/restaurants") {
        get() {
            call.respondText { "GET api/v1/restaurants" }
        }

        get("{id}") {
            call.respondText { "GET api/v1/restaurants/:id" }
        }

        post() {
            call.respondText { "POST api/v1/restaurants/:id" }
        }

        put("{id}") {
            call.respondText { "PUT api/v1/restaurants/:id" }
        }

        delete ("{id}") {
            call.respondText { "DELETE api/v1/restaurants/:id" }
        }
    }
}

You can test these endpoints with your browser or with postman as we have shown below. At this point, our endpoints are not doing anything. But we can show that client API requests are correctly routed to our application interface.

Ktor web api folder structure with routing.
Folder structure with routes
Postman example to call Ktor web API