Building a Backend Service with Ktor

Ktor is a lightweight framework written in Kotlin programming language. This framework allows us to create asynchronous servers and clients. In this article, I will explain the basics of Ktor framework by creating a simple REST service.

Getting Started

To start, You’ll need IntelliJ IDEA with the Ktor plugin installed. IntelliJ is an IDE developed by JetBrains. You can download IntelliJ IDEA from jetbrains.com/idea/.

To install the Ktor plugin, start IntelliJ and select Help ▸ Settings on Windows or IntelliJ menu ▸ Preferences on Mac.

Then select the Plugins section and search for Ktor. Install the plugin and restart IntelliJ.

Starting a project in IntelliJ

To start a new project in IntelliJ Go to New ▸ Project

The project type has to be ktor, if ktor is not on the list, restart IntelliJ, also make sure Gradle is the Project type and Netty for server Type.

Next, select your features. Under Features under the server section, choose Locations, Sessions and Routing. Locations and Routing handle API routes. Sessions keep track of the current user so you have a state associated with it.

For Authentication, choose Authentication JWT – JSON Web Token – to create JSON-based access tokens. For Content Negotiation, choose GSON, a library used to serialize and deserialize Java objects to and from JSON.

Select Next and on the next screen, set the GroupId to com.example and the ArtifactId to employee Then select Next. we’ll be creating a server to create new employees in a company

Click Next to and set the name of the project to Employee and set project location, then click Finish

to allow project creation. After the project has been created, go to Build ▸ Build Project and ensure your project builds without errors.

Click on the pink hammer to ADD CONFIGURATION to your project at the top right corner of the IDE, this will enable us to run the boilerplate code generated in the Application.Kt file when the project was created “Hello World“.

Click on the add button(+), pick kotlin and under Use classpath of module and set to employee.main and under Main class choose the com.example.Application.kt and click Ok

Before running the app, the application.conf can be adjusted to the preferred port and host address, it is advisable to change from 8080 and 0.0.0.0 which is the default port and host respectively because the 8080 port is always most likely in use by another service on the computer

The port was changed to 5000 and the host was changed to a custom one 127.0.0.1 and run the App, you should see Responding at http://127.0.0.1:5000. Click on the link and you should see “hello world“ on the webpage.

Remove everything in routing section, Type and MyLocation under Application.kt

Implementing Routes

Your next step is to implement the employee API

Here’s how the API for employees will look:

  • employees/create (POST): Creates a new employee. Passes in the email address, name and password.

  • employees/login (POST): Logs in an employee using the email address and password

Define the Routes

In Ktor, a Route defines a path to your server. This could be an API, like the one you’re creating, or your home page. For example

1 2 3 get("/") { call.respondText("HELLO WORLD!", contentType = ContentType.Text.Plain) }

This is a GET that responds to the string path “/” (i.e., the root path) with the text “HELLO WORLD!”

Setting up Postgres

Ktor supports Postgres. On Windows, download your preferred tool at postgresql.org.

On Mac, download the Postgres app from postgresapp.com.

Also, you can download pgAdmin from https://www.pgadmin.org/download/, this can serve as an Admin GUI for managing servers/databases

Once you’ve installed Postgres, create a database, which you’ll name employees for this project. To create it, run Postgres from the command line with:

1 psql -U postgres

Once in Postgres, create the database by typing:

1 create database employees;

Note that you need the semi-colon at the end.

To connect to the database, type:

1 \c employees

The tables do not need to be created, creation of tables will be handled with code

Database Dependencies

To access the database you’ve just created, you’ll add all the libraries the project needs:

  1. Exposed: A JetBrains library you use to easily access a database.
  2. Hikari: Use this library to set up the configuration for the Postgres database.
  3. postgresql: Provides the JDBC driver, which allows the code to interact with the database.

Open gradle.properties and add the following variables:

1 2 3 exposed_version=0.18.1 hikaricp_version=3.3.1 postgres_version=42.2.4.jre7

open build.gradle and add the following dependencies

1 2 3 4 5 compile "org.jetbrains.exposed:exposed-core:$exposed_version" compile "org.jetbrains.exposed:exposed-dao:$exposed_version" compile "org.jetbrains.exposed:exposed-jdbc:$exposed_version" compile "org.postgresql:postgresql:$postgres_version" compile "com.zaxxer:HikariCP:$hikaricp_version"

 

In IntelliJ, open up the Gradle tab and choose sync.

Run Server

We’ll be using environment variables System.getEnv() to connect safely to our database

click COM.EXAMPLE.APPLICATION.KT then click the edit configuration to edit the current configuration

 

Under the Environment Variables section, click the button on the far right and add the following parameters:

1 2 3 4 JDBC_DRIVER=org.postgresql.Driver JDBC_DATABASE_URL=jdbc:postgresql:employees?user=postgres&password=mypassword SECRET_KEY=898748674728934843 JWT_SECRET=898748674728934843

Here’s what these parameters are doing:

  • JDBC_DRIVER: Sets the driver for Postgres.

  • JDBC_DATABASE_URL: The database connection URL.

  • SECRET_KEY: Use this for hashing.

  • JWT_SECRET: You’ll use this later for authentication.

The Environment Variable screen should look like this:

Be sure there are no extra spaces around the keys or values then click OK, then click OK to save configurations. Now click the green run button in the toolbar to build and run. There should be no errors.

Adding a Data Layer

The data layer provides a transparent layer that interacts with the underlying data store — Postgres, in our case. To achieve this, you’ll use the repository pattern.

In the following code, you’ll learn how to create the data models and classes that describe the tables for storing data. You’ll also learn how to implement a repository interface.

Setting up Model Classes

Before you can hook up the database, you need model classes. This project requires an Employee data class.

Create a file called Employee under the model folder and add the following:

1 2 3 4 5 6 7 8 9 import io.ktor.auth.Principal import java.io.Serializable data class Employee( val userId: Int, val email: String, val displayName: String, val passwordHash: String ) : Serializable, Principal

 

This creates an Employee class with an email and display name. You’ll store the password as a hash value to protect it should the database be compromised.

Database class

Create a repository package/folder and under that create an Employees object, we will be using this object to create our database table

1 2 3 4 5 6 7 8 9 import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.Table object Employees : Table() { val userId : Column<Int> = integer("id").autoIncrement().primaryKey() val email = varchar("email", 128).uniqueIndex() val displayName = varchar("display_name", 256) val passwordHash = varchar("password_hash", 64) }

The table comes from the Exposed library. You can use the Column class or helpers like varchar to define the fields in the table. autoIncrement automatically creates IDs for new entries.

Create a file named DatabaseFactory to contain the class for connecting to the database.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.SchemaUtils import org.jetbrains.exposed.sql.transactions.transaction object DatabaseFactory { fun init() { Database.connect(hikari()) // 1 // 2 transaction { SchemaUtils.create(Employees) } }

 

  1. The database is from the Exposed library. It allows you to connect to the database with HikariDataSource, which your Hikari method creates.

  2. You use a transaction to create your Employees table. It will only create the table if they don’t already exist.

The hikari method is set up as below:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 private fun hikari(): HikariDataSource { val config = HikariConfig() config.driverClassName = System.getenv("JDBC_DRIVER") // 1 config.jdbcUrl = System.getenv("JDBC_DATABASE_URL") // 2 config.maximumPoolSize = 3 config.isAutoCommit = false config.transactionIsolation = "TRANSACTION_REPEATABLE_READ" val user = System.getenv("DB_USER") // 3 if (user != null) { config.username = user } val password = System.getenv("DB_PASSWORD") // 4 if (password != null) { config.password = password } config.validate() return HikariDataSource(config) } // 5 suspend fun <T> dbQuery(block: () -> T): T = withContext(Dispatchers.IO) { transaction { block() } } }

Steps 1 through 4 in the code above use the Environment Variables that you defined earlier.

Step 5 declares a helper function to wrap a database call in a transaction and have it run on an IO thread. This function uses Coroutines.

Repository

In this section, you’ll work on adding a repository interface to the project. The interface will wrap all calls to the database. Create a new file in the repository folder named Repository

1 2 3 4 5 6 7 8 9 import com.example.model.Employee interface Repository { suspend fun addEmployee(email: String, displayName: String, passwordHash: String): Employee? suspend fun findEmployee(userId: Int): Employee? suspend fun findEmployeeByEmail(email: String): Employee? }

This creates an interface that includes functions for adding and finding Users by ID and email. This is the entry point to interact with the data layer.

Now, implement this interface by creating a file named EmployeeRepository in the repository folder. Add the following:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import com.example.model.Employee import com.example.repository.DatabaseFactory.dbQuery class EmployeeRepository: Repository { override suspend fun addEmployee( email: String, displayName: String, passwordHash: String) : Employee? { TODO("not implemented") } override suspend fun findEmployee(userId: Int) = dbQuery { TODO("not implemented") } override suspend fun findEmployeeByEmail(email: String)= dbQuery { TODO("not implemented") } }

Start with addEmployee by replacing its body with the following. This function will return an employee if one is successfully created.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 override suspend fun addEmployee( email: String, displayName: String, passwordHash: String) : Employee? { var statement: InsertStatement<Number>? = null // 1 dbQuery { // 2 // 3 statement = Employees.insert { employee -> employee[Employees.email] = email employee[Employees.displayName] = displayName employee[Employees.passwordHash] = passwordHash } } // 4 return rowToUser(statement?.resultedValues?.get(0)) }
  1. InsertStatement: An Exposed class that helps with inserting data.
  2. dbQuery: A helper function, defined earlier, that inserts a new Employee record.
  3. Uses the insert method from the Employees parent class to insert a new record.
  4. rowToUser: A private function required to convert the Exposed ResultRow to your Employee class.

Now, you need to add the definition of rowToUser:

1 2 3 4 5 6 7 8 9 10 11 private fun rowToUser(row: ResultRow?): Employee? { if (row == null) { return null } return Employee( userId = row[Employees.userId], email = row[Employees.email], displayName = row[Employees.displayName], passwordHash = row[Employees.passwordHash] ) }

 

For the two other methods in the EmployeeRepository which are findEmployee and findEmployeeByEmai

1 2 3 4 5 6 7 8 override suspend fun findEmployee(userId: Int) = dbQuery { Employees.select { Employees.userId.eq(userId) } .map { rowToUser(it) }.singleOrNull() } override suspend fun findEmployeeByEmail(email: String) = dbQuery { Employees.select { Employees.email.eq(email) } .map { rowToUser(it) }.singleOrNull() }

Run a Build ▸ Build Project to ensure your project builds without errors.

Authentication

You need to authenticate users to keep your server secure. When you created your server, you chose the JWT authentication feature. But you’ll need to create a few functions to use it.

create an auth package/folder then create an Auth.kt file under it then include the following:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import io.ktor.util.KtorExperimentalAPI import io.ktor.util.hex import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec @KtorExperimentalAPI // 1 val hashKey = hex(System.getenv("SECRET_KEY")) // 2 @KtorExperimentalAPI val hmacKey = SecretKeySpec(hashKey, "HmacSHA1") // 3 @KtorExperimentalAPI fun hash(password: String): String { // 4 val hmac = Mac.getInstance("HmacSHA1") hmac.init(hmacKey) return hex(hmac.doFinal(password.toByteArray(Charsets.UTF_8))) }
  1. Makes use of the SECRET_KEY Environment Variable defined in step Use this value as the argument of the hex function, which turns the HEX key into a ByteArray. Note the use of @KtorExperimentalAPI to avoid warnings associated with the experimental status of the hex function.
  2. Defines Environment Variable.
  3. Creates a secret key using the given algorithm, HmacSHA1.
  4. hash converts a password to a string hash.

Next, create a new class in auth named JwtService and add the following:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import com.auth0.jwt.JWT import com.auth0.jwt.JWTVerifier import com.auth0.jwt.algorithms.Algorithm import com.example.model.Employee import java.util.* class JwtService { private val issuer = "employee" private val jwtSecret = System.getenv("JWT_SECRET") // 1 private val algorithm = Algorithm.HMAC512(jwtSecret) // 2 val verifier: JWTVerifier = JWT .require(algorithm) .withIssuer(issuer) .build() // 3 fun generateToken(employee: Employee): String = JWT.create() .withSubject("Authentication") .withIssuer(issuer) .withClaim("id", employee.userId) .withExpiresAt(expiresAt()) .sign(algorithm) private fun expiresAt() = Date(System.currentTimeMillis() + 3_600_000 * 24) // 24 hours

The previous code requires the JWT_SECRET Environment Variable in step 1 to create the JWTVerifier in step 2. generateToken, defined in step 3, generates a token that the API uses to authenticate the request. You’ll need this function later.

Next, create a file named MySession in the auth folder and add this code:

1 data class MySession(val userId: Int)

This stores the current userId in a Session.

Configuring Application

In this section, you’ll configure the server. To do so, you’ll use some of the methods you created earlier.

Open Application.kt, remove MySession and import the newer MySession you just created.

Add the following after the install(Sessions) section:

1 2 3 4 5 6 // 1 DatabaseFactory.init() val db = EmployeeRepository() // 2 val jwtService = JwtService() val hashFunction = { s: String -> hash(s) }+

The first part of this code initializes the data layer you defined earlier, while the second part handles authentication.

Next, add the following to the install(Authentication) section:

1 2 3 4 5 6 7 8 9 10 11 jwt("jwt") { //1 verifier(jwtService.verifier) // 2 realm = "employee server" validate { // 3 val payload = it.payload val claim = payload.getClaim("id") val claimString = claim.asInt() val user = db.findUser(claimString) // 4 user } }

You define the JWT name in step 1, which can be anything you want.

Step 2 specifies the verifier you created in the JwtService class.

Step 3 creates a method that runs each time the app needs to authenticate a call.

Finally, step 4 tries to find the user in the database with the userId from claimString. If the userID exists, it verifies the user. Otherwise, it returns a null user and rejects the route.

Before you continue, run your server to make sure everything compiles and runs properly. You should see several Hikari debug statements in the output window.

Adding the Employee Create Route

Create a Route.kt file

These are the imports you’ll need for the Route file

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import com.example.auth.JwtService import com.example.auth.MySession import com.example.repository.Repository import io.ktor.application.application import io.ktor.application.call import io.ktor.application.log import io.ktor.http.HttpStatusCode import io.ktor.http.Parameters import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.Location import io.ktor.locations.post import io.ktor.request.receive import io.ktor.response.respond import io.ktor.response.respondText import io.ktor.routing.Route import io.ktor.sessions.sessions import io.ktor.sessions.set

Then add the route constants we will be using

1 2 3 const val USERS = "employees" const val USER_LOGIN = "$USERS/login" const val USER_CREATE = "$USERS/create"

These define the strings needed for the routes, allowing you to create or log in an employee

Add the following route classes:

1 2 3 4 5 6 7 @KtorExperimentalLocationsAPI @Location(USER_LOGIN) class EmployeeLoginRoute @KtorExperimentalLocationsAPI @Location(USER_CREATE) class EmployeeCreateRoute

@Location(USER_LOGIN) associates the string USER_LOGIN with the UserLoginRoute class.

Then add the employee create a route:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @KtorExperimentalLocationsAPI // 1 fun Route.employees( db: Repository, jwtService: JwtService, hashFunction: (String) -> String ) { post<EmployeeCreateRoute> { // 2 val signupParameters = call.receive<Parameters>() // 3 val password = signupParameters["password"] // 4 ?: return@post call.respond( HttpStatusCode.Unauthorized, "Missing Fields") val displayName = signupParameters["displayName"] ?: return@post call.respond( HttpStatusCode.Unauthorized, "Missing Fields") val email = signupParameters["email"] ?: return@post call.respond( HttpStatusCode.Unauthorized, "Missing Fields") val hash = hashFunction(password) // 5 try { val newUser = db.addEmployee(email, displayName, hash) // 6 newUser?.userId?.let { call.sessions.set(MySession(it)) call.respondText( jwtService.generateToken(newUser), status = HttpStatusCode.Created ) } } catch (e: Throwable) { application.log.error("Failed to register user", e) call.respond(HttpStatusCode.BadRequest, "Problems creating User") } } }

Here’s how all that breaks down.

  1. Defines an extension function to Route named users that takes in a Repository, a JWTService and a hash function.
  2. Generates a route for creating a new user.
  3. Uses the call parameter to get the parameters passed in with the request.
  4. Looks for the password parameter and returns an error if it doesn’t exist.
  5. Produces a hash string from the password.
  6. Adds a new employee to the database.

Add the following code to the routing section in Application.kt:

1 employees(db, jwtService, hashFunction)

Perform a Build ▸ Build Project to ensure your project still builds without errors

Then Run the server

Test Routes

The Postman app is a terrific tool for testing APIs. Find it at getpostman.com/.

Once installed, open Postman and use a new tab to make a POST request to generate a new user. Use localhost:5000/employees/create. In the Body tab, add these three variables: displayName, email and password.

Use any data you want in the Value column. Press Send and you’ll get a token back.

To save that token in Postman, set a global variable that you can use in other calls. To do so, go to the Tests tab and add:

1 2 postman.clearGlobalVariable("jwt_token"); postman.setGlobalVariable("jwt_token", data);

Customize Response from Server

We will have to add files to models namely the payload and SignUpResponse under the models package:

1 2 3 4 5 6 7 8 9 10 11 12 data class Payload ( var userId: String, var name: String, var email: String, var password: String ) data class SignUpResponse( var status: HttpStatusCode, var message: String, var payload: com.example.model.Payload, var token: String )

Then replace the code below under the Routes

1 2 3 4 call.respondText( jwtService.generateToken(newUser), status = HttpStatusCode.Created )

replace with:

1 2 3 4 5 6 7 8 call.respond( SignUpResponse( HttpStatusCode.Created, "user successfully created", Payload( newUser.userId.toString(), displayName, email, hash ), jwtService.generateToken(newUser) ) )

 

Then rerun the server and fill in the appropriate details on postman:

You get the details of the just created user/employee in a JSON format

This service listens for only POST requests and performs create database operation with Postgres, you can perform other requests such as GET, PUT and DELETE requests and perform CRUD database operations with Postgres

If you want to learn more about APIs, visit these links

  1. How APIs work — An Analogy For Dummies https://medium.com/@tyteen4a03/how-apis-work-an-analogy-for-dummies-ac6ee1d1671b
  2. How do APIs work? https://tray.io/blog/how-do-apis-work

if you want to see more of ktor support visit ktor.io/