Ktor: REST API for Mobile

In this tutorial, you’ll create a REST API server for mobile apps using the new Ktor framework from JetBrains. By Kevin D Moore.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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 a User and a Todo.

Create a file called User under the models folder and add the following:

import io.ktor.auth.Principal
import java.io.Serializable

data class User(
    val userId: Int,
    val email: String,
    val displayName: String,
    val passwordHash: String
) : Serializable, Principal

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

Next, create a new file in models called Todo and add the following:

data class Todo(
    val id: Int,
    val userId: Int, 
    val todo: String, 
    val done: Boolean
)

This defines what a TODO is and ties it to the User with the userId field. The user will enter the todo field as the text for the Todo.

The done field will allow the user to mark the Todo as done or incomplete.

Working on the Database Classes

It’s time to work on those database classes. Create a file named Users under the repository folder and add the following:

import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.Table

object Users : 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)
}

Table comes from the Exposed library. You can use the Column class or helpers like varchar to define the fields in the table. autoIncrement is a nice way to have the database automatically create IDs for new entries.

Next, create a file named Todos and add the following code:

import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.Table

object Todos: Table() {
    val id : Column<Int> = integer("id").autoIncrement().primaryKey()
    val userId : Column<Int> = integer("userId").references(Users.userId)
    val todo = varchar("todo", 512)
    val done = bool("done")
}

This table has its own id plus a userId so you can retrieve all the TODOs for a specific user.

Create a file named DatabaseFactory to contain the class for connecting to the database. Add the following code to the file:

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(Users) 
            SchemaUtils.create(Todos)
        }
    }
  1. 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 Users and Todos tables. It will only create the tables if they don’t already exist.

At this point, you need to set up the hikari method. Add the following code after init:

    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() }
        }
}

As you can see, steps 1 through 4 in the code above use the Environment Variables that you defined earlier. This tutorial doesn’t use steps 3 and 4. They’re there because you need them if you deploy your server to a website such as Heroku or Google Cloud.

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.

Coroutines are beyond the scope of this tutorial. But if you want to learn more about them, kotlinlang.org’s Coroutines Guide is an excellent place to start.

Adding Your 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 and add the following:

import com.raywenderlich.models.Todo
import com.raywenderlich.models.User

interface Repository {
    suspend fun addUser(email: String,
                        displayName: String,
                        passwordHash: String): User?
    suspend fun findUser(userId: Int): User?
    suspend fun findUserByEmail(email: String): User?
}

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.

Note: The suspend keyword marks the function as suspending, meaning you can pause it and resume at a later stage. It’s the mechanism behind Coroutines.

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

import com.raywenderlich.models.Todo
import com.raywenderlich.models.User
import com.raywenderlich.repository.DatabaseFactory.dbQuery
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.statements.InsertStatement

class TodoRepository: Repository {
    override suspend fun addUser(
          email: String,
          displayName: String,
          passwordHash: String) : User? {
        TODO("not implemented")
    }

    override suspend fun findUser(userId: Int) = dbQuery {
        TODO("not implemented")            
    }

    override suspend fun findUserByEmail(email: String)= dbQuery {
        TODO("not implemented")
    }
}

You’ll implement each of the interface’s methods by replacing TODO("not implemented").

Start with addUser by replacing its body with the following. This function will return a user if one is successfully created.

override suspend fun addUser(
      email: String,
      displayName: String,
      passwordHash: String) : User? {
    var statement : InsertStatement<Number>? = null // 1
    dbQuery { // 2
       // 3
       statement = Users.insert { user ->
           user[Users.email] = email
           user[Users.displayName] = displayName
           user[Users.passwordHash] = passwordHash
       }
    }
    // 4
    return rowToUser(statement?.resultedValues?.get(0))
}

This is what you’re doing here:

  1. InsertStatement: An Exposed class that helps with inserting data.
  2. dbQuery: A helper function, defined earlier, that inserts a new User record.
  3. Uses the insert method from the Users parent class to insert a new record.
  4. rowToUser: A private function required to convert the Exposed ResultRow to your User class.

Now, you need to add the definition of rowToUser:

private fun rowToUser(row: ResultRow?): User? {
    if (row == null) {
        return null
    }
    return User(
        userId = row[Users.userId],
        email = row[Users.email],
        displayName = row[Users.displayName],
        passwordHash = row[Users.passwordHash]
    )
}

Next, define the body of the missing selection functions, findUser and findUserByEmail. To do so, add the following code:

override suspend fun findUser(userId: Int) = dbQuery {
     Users.select { Users.userId.eq(userId) }
        .map { rowToUser(it) }.singleOrNull()
}

override suspend fun findUserByEmail(email: String)= dbQuery {
     Users.select { Users.email.eq(email) }
        .map { rowToUser(it) }.singleOrNull()
}

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