developers

Creating an HTTP API with Ktor and Kotlin

Explore the details of a simple Ktor application exposing an HTTP API

Ktor is a modern web framework developed by JetBrains. The library is written in Kotlin and takes advantage of coroutines to build asynchronous and non-blocking services.

To demonstrate Ktor we'll build a simple HTTP API exposing three endpoints:

  • /api/messages/public
  • /api/messages/protected
  • /api/messages/admin

We'll build this API over the course of three posts. In this first post, we'll build the HTTP API and provide public access to all endpoints. In the second post, we'll update the API to require a valid access token to query the

protected
and
admin
endpoints. Finally, in the third post, we'll implement Role-Based Access Control (RBAC) by requiring special permissions to query the
admin
endpoint.

In subsequent posts, we'll access this API via a frontend application, written for Node.js, which understands how to call the three endpoints listed above.

The Sample Application Code

To build the backend application, you'll need to have JDK 11 or above, which is available from many sources, including OpenJDK, AdoptOpenJDK, Azul, or Oracle.

The final code for the Ktor application can be found here. The code discussed in each post is matched by a branch:

  • starter — the base API with no authentication or authorization. This branch is discussed in this post.
  • add-authorization — the API requiring a valid access token for the
    protected
    and
    admin
    endpoints.
  • add-rbac — the API requiring special permissions to access the
    admin
    endpoint.

The frontend application code can be found here.

Bootstrapping the Ktor Application

To bootstrap our project, we'll use the Ktor Project Generator. Our application only needs the Authentication server dependency.

The project can be built with Gradle, Gradle with Kotlin DSL, or Maven. This post will use the Gradle with Kotlin DSL option, although any build tool will do. It is convenient to use the wrapper option to have the appropriate build tool version downloaded, so we'll check the with Wrapper option.

Click the Build button to download the bootstrapped application: Build

The Ktor project generator.

Customizing the Ktor application

The generated application includes a file called

src/Application.kt
that holds the code for our API. We'll tweak the contents of this file to support CORS and expose our three endpoints. The complete file is shown below:

package com.matthewcasperson

import io.ktor.application.*
import io.ktor.features.*
import io.ktor.http.*
import io.ktor.response.*
import io.ktor.routing.*

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

fun Application.module() {

    install(CORS) {
        anyHost()
        method(HttpMethod.Options)
        method(HttpMethod.Get)
        header("authorization")
        allowCredentials = true
        allowNonSimpleContentTypes = true
    }

    routing {
        get("/api/messages/public") {
            call.respondText(
                """{"message": "The API doesn't require an access token to share this message."}""",
                contentType = ContentType.Application.Json
            )
        }
    }

    routing {
        get("/api/messages/protected") {
            call.respondText(
                """{"message": "The API successfully validated your access token."}""",
                contentType = ContentType.Application.Json
            )
        }
    }

    routing {
        get("/api/messages/admin") {
            call.respondText(
                """{"message": "The API successfully recognized you as an admin."}""",
                contentType = ContentType.Application.Json
            )
        }
    }
}

Let's break this code down.
We start with the application entry point using the EngineMain class.

EngineMain
starts a server with the selected engine and loads the application module specified in the external
resources/application.conf
file:

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

We then define our application module. A module is defined as:

A user-defined function receiving the Application class that is in charge of configuring the server pipeline, install plugins, registering routes, handling requests, etc.

fun Application.module() {

Because this API will be called from a frontend application either running from a different domain or a different port, we need to install the Cross-Origin Resource Sharing (CORS) feature for our API.

We define the following options:

  • anyHost()
    allows a webpage running on any host or port to access this API.
  • method(HttpMethod.Options)
    allows remote browsers to make HTTP Options calls.
  • method(HttpMethod.Get)
    allows remote browsers to make HTTP Get calls.
  • header("authorization")
    allows the
    authorization
    header to be passed from a remote browser. We don't use this header at this stage in the API development, but the frontend will still send it, so we must accept it.
  • allowCredentials = true
    allows the frontend to send credentials with a request.
  • allowNonSimpleContentTypes = true
    allows our API to return a JSON response.
    install(CORS) {
        anyHost()
        method(HttpMethod.Options)
        method(HttpMethod.Get)
        header("authorization")
        allowCredentials = true
        allowNonSimpleContentTypes = true
    }

The remaining code configures the three endpoints exposed by our API.

Whereas other popular JVM platforms like Spring or Quarkus tend to use methods and annotations to expose services, Ktor provides a Domain Specific Language (DSL). The result is that our endpoints are constructed with a series of trailing lambdas, providing the appearance of multiple nested functions.

Here we define three routes, one for each of the endpoints listed in the post introduction, with each returning a hardcoded JSON blob:

routing {
        get("/api/messages/public") {
            call.respondText(
                """{"message": "The API doesn't require an access token to share this message."}""",
                contentType = ContentType.Application.Json
            )
        }
    }

    routing {
        get("/api/messages/protected") {
            call.respondText(
                """{"message": "The API successfully validated your access token."}""",
                contentType = ContentType.Application.Json
            )
        }
    }

    routing {
        get("/api/messages/admin") {
            call.respondText(
                """{"message": "The API successfully recognized you as an admin."}""",
                contentType = ContentType.Application.Json
            )
        }
    }

Updating the Configuration

We need to change the default port the application listens to. Here we update the

resources/application.conf
file to define the default port as
6060
.

Also note the

application
setting, which, as we noted earlier, is read by the
EngineMain
class to define the entrypoint of our application:

ktor {
    deployment {
        port = 6060
        port = ${?PORT}
    }
    application {
        modules = [ com.matthewcasperson.ApplicationKt.module ]
    }
}

Compiling and Running the Application

Compile the application with the Gradle wrapper provided as part of the generated project:

./gradlew installDist

Then run it with:

./build/install/ktor-demo/bin/ktor-demo

Our backend API is now ready to accept connections on http://localhost:6060.

Open up http://localhost:6060/api/messages/public, http://localhost:6060/api/messages/protected, or http://localhost:6060/api/messages/admin from a web browser to view the JSON response.

With that, we have the initial implementation of our Ktor API.

Conclusion

In this post, we created a simple HTTP API with Ktor exposing three endpoints. These endpoints are all publicly accessible, and in the next post, we'll update the API to require a valid access token generated by an Auth0 application.