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
andprotected
endpoints.admin
- add-rbac — the API requiring special permissions to access the
endpoint.admin
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:
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:
allows a webpage running on any host or port to access this API.anyHost()
allows remote browsers to make HTTP Options calls.method(HttpMethod.Options)
allows remote browsers to make HTTP Get calls.method(HttpMethod.Get)
allows theheader("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.authorization
allows the frontend to send credentials with a request.allowCredentials = true
allows our API to return a JSON response.allowNonSimpleContentTypes = true
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.
About the author
Matthew Casperson
Product Manager at Octopus Deploy