Android & Kotlin Tutorials

Learn Android development in Kotlin, from beginner to advanced.

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.

4.8/5 5 Ratings

Version

  • Kotlin 1.3, macOS 10.14, IntelliJ IDEA

Ktor is a modern framework provided by JetBrains. Written in Kotlin, it changes the way servers are built.

Instead of a configuration-heavy framework like Spring, Ktor uses a simple domain-specific language (DSL) for installing features to meet your server needs. These features include authentication, sessions, routing, web templates, JSON wrapping and more.

In this tutorial, you’ll focus on using Ktor to create REST APIs for mobile — though you can use it for the web, too. You’ll create a Ktor REST API server named TodoServer. This server will authenticate users and provide an API for users and TODOs.

Getting Started

To start, download the begin project by clicking the Download Materials button at the top or bottom of the tutorial.

Next, you’ll need IntelliJ IDEA with the Ktor plugin. IntelliJ is an integrated development environment (or IDE) also developed by JetBrains. Find IntelliJ at jetbrains.com/idea/.

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

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

At this point, you can continue this tutorial in one of two ways: If you want to learn how to start a new project from scratch, follow the steps below. Otherwise, open the begin project and jump to the next section.

Starting a Project in IntelliJ

To start from scratch, you’ll begin a new project in IntelliJ. Choose New ▸ New Project.

Starting a new project in IntelliJ

For the project type, choose Ktor. Double-check that the project type is Gradle and it uses Netty for the server type.

Next, select your features. Under Features, choose Locations, Sessions and Routing. Locations and Routing handle API routes. Sessions keeps track of the current user so you have a state associated with it. Since you’re creating a REST API, you won’t need a templating engine.

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.

On this screen, set the GroupId to com.raywenderlich and the ArtifactId to todoserver. Then select Next.

New project set-up screen

Set the Project name to TodoServer, and choose the directory where you want your server stored. Then select Finish to let the IDE create the project.

New project name and location screen in IntelliJ

Now go to Build ▸ Build Project. Ensure your project builds without errors.

IntelliJ successful build

Finally, you’ll clean up some code you don’t need. Open Application.kt. Remove everything in the routing section, then remove MyLocation and Type. You’ll remove MySession later.

Great! You’re ready to shape your API with Ktor.

Implementing APIs

Your next step is to implement two APIs: one for handling users and one for TODOs.

Here’s how your API for users will look:

  • v1/users/create (POST): Creates a new user. Passes in the email address, name and password.
  • v1/users/login (POST): Logs in a user. Passes in the email address and password.

Here’s the API for TODOs:

  • v1/todos (POST): Creates a new TODO. Passes in the TODO string and done (true/false).
  • v1/todos (GET): Gets a list of TODOs for the current user.
Note: The routes start with v1 so that if the API changes, you can add a v2 version that can coexist with v1 without breaking it.

Defining 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. Ktor uses GET/POST/PUT/DELETE/REST along with a path. For example:

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!”

There are two different route types:

  • String Route: Uses a path in a string like: /shopping/store/cart.
  • Class Route: Uses a class instead of strings. This makes it easier to read and allows you to put all the functionality of a route in a separate class.

You’ll use Class Routes in this project.

To use Class Routes, define a class and annotate it with the annotation @Location("route"). Create an extension function from Route where you’ll define your routes. You can define multiple GET/POST/DELETE-type routes in the same function.

Setting up Postgres

Ktor supports Postgres. There are several tools to run a Postgres server.

On Mac, download the Postgres app from postgresapp.com. On Windows, download your preferred tool at postgresql.org.

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

psql -U postgres

Once in Postgres, create the database by typing:

create database todos;

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

To connect to the database, type:

\c todos

Finally, to exit press Ctrl-D. You don’t need to create the tables; the code will handle it.

Setting up 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:

exposed_version=0.18.1
hikaricp_version=3.3.1
postgres_version=42.2.4.jre7

This sets the versions for the libraries you need. Then open build.gradle and add the following in the dependencies section:

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.

Now that your databases are ready, it’s time to make sure your server can run.

Running the Server

You’ll use System.getEnv() to read in variables the server will use to connect to the proper database.

First, click the Add Configuration… button in the toolbar.

Toolbar with the Add Configuration... button

Next, click the + button in the top left. From the pop-up menu, choose Kotlin.

Run/Debug Configuration Page setup

Enter Server as the Name of the configuration. Under Use classpath of Module, choose todoserver.main. In the Main class field, click on the “…” button and select ApplicationKt.

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

JDBC_DRIVER=org.postgresql.Driver
JDBC_DATABASE_URL=jdbc:postgresql:todos?user=postgres;
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.

Your Environment Variable screen should look like this:

Environment Variable screen setup

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

IntelliJ successful build

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.

Authenticating Your Users

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.

Start by creating a new file titled Auth.kt under auth and add the following:

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 2. 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:

import com.auth0.jwt.JWT
import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.Algorithm
import com.raywenderlich.models.User
import java.util.*

class JwtService {

    private val issuer = "todoServer"
    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(user: User): String = JWT.create()
        .withSubject("Authentication")
        .withIssuer(issuer)
        .withClaim("id", user.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:

data class MySession(val userId: Int)

This stores the current userId in a Session.

Configuring Application

In this section, you’ll add the required plumbing to configure the server. To do so, you’ll use some of the functions you created earlier.

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

Now, add the following after the install(Sessions) section:

// 1
DatabaseFactory.init()
val db = TodoRepository()
// 2
val jwtService = JwtService()
val hashFunction = { s: String -> hash(s) }

Import these classes. 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:

jwt("jwt") { //1
    verifier(jwtService.verifier) // 2
    realm = "Todo 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.

Hikari debug statements

Building the Routes

If you started your own project, here’s an extra step for you. At the bottom of Application.kt, add the following constant:

const val API_VERSION = "/v1"

This is a constant that you will use to prefix all the paths in your routes.

Adding the User Create Route

In Routes, create a new file named UserRoute.

Start by adding the following imports to the top of the file:

import com.raywenderlich.API_VERSION
import com.raywenderlich.auth.JwtService
import com.raywenderlich.auth.MySession
import com.raywenderlich.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

These will avoid issues of choosing the wrong version of a class. Next, add some route constants:

const val USERS = "$API_VERSION/users"
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 a user.

Add the following route classes:

@KtorExperimentalLocationsAPI
@Location(USER_LOGIN)
class UserLoginRoute

@KtorExperimentalLocationsAPI
@Location(USER_CREATE)
class UserCreateRoute

Since Locations API is experimental, using @KtorExperimentalLocationsAPI removes any compiler warnings. @Location(USER_LOGIN) associates the string USER_LOGIN with the UserLoginRoute class.

Then add the user create route:

@KtorExperimentalLocationsAPI
// 1
fun Route.users(
    db: Repository, 
    jwtService: JwtService, 
    hashFunction: (String) -> String
) {  
    post<UserCreateRoute> { // 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.addUser(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 user to the database.
  7. But you’re not done yet. Add the following code to the routing section in Application.kt:

    users(db, jwtService, hashFunction)
    

    Build and run the server again.

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

    Testing 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:8080/v1/users/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.

    Testing routes in Postman

    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:

    var data = responseBody;
    postman.clearGlobalVariable("jwt_token");
    postman.setGlobalVariable("jwt_token", data);
    

    This sets a variable named jwt_token that you can use in other calls.

    Setting up the jwt_token

    Now that your server can generate jwt tokens, its time to add some routes to perform authentication.

    Adding the User Login Route

    Open UserRoutes and add the following after the UserCreateRoute:

        post<UserLoginRoute> { // 1
            val signinParameters = call.receive<Parameters>()
            val password = signinParameters["password"]
                    ?: return@post call.respond(
                           HttpStatusCode.Unauthorized, "Missing Fields")
            val email = signinParameters["email"] 
                    ?: return@post call.respond(
                           HttpStatusCode.Unauthorized, "Missing Fields")
            val hash = hashFunction(password)
            try {
                val currentUser = db.findUserByEmail(email) // 2
                currentUser?.userId?.let {
                    if (currentUser.passwordHash == hash) { // 3
                        call.sessions.set(MySession(it)) // 4
                        call.respondText(jwtService.generateToken(currentUser)) // 5
                    } else {
                        call.respond(
                           HttpStatusCode.BadRequest, "Problems retrieving User") // 6
                    }
                }
            } catch (e: Throwable) {
                application.log.error("Failed to register user", e)
                call.respond(HttpStatusCode.BadRequest, "Problems retrieving User")
            }
        }
    

    Here’s the breakdown.

    1. Defines a POST for logging in a user.
    2. Tries to find the user by their email address.
    3. Compares the hash created from the passed-in password with the existing hash.
    4. If the hashes don’t match, the method returns an error. Otherwise, it sets the current session for the user ID in step 4 and creates a token from the user to return in step 5.

    Test the Server

    Restart your server and open Postman. Use a new tab to create a POST request to log in a user. Use localhost:8080/v1/users/login.

    In the Body tab, add two variables: email and password using the data from the create call in the Value column. Press Send and you’ll get a token back.

    Postman setup

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

    Adding the TODO Routes

    Before adding the TODO routes, you need to adapt the repository. Open Repository and after findUserByEmail add:

        suspend fun addTodo(userId: Int, todo: String, done: Boolean): Todo?
        suspend fun getTodos(userId: Int): List<Todo>
    

    Open TodoRepository and add after rowToUser:

        // 1
        override suspend fun addTodo(userId: Int, todo: String, done: Boolean): Todo? {
            var statement : InsertStatement<Number>? = null
            dbQuery {
                statement = Todos.insert {
                    it[Todos.userId] = userId
                    it[Todos.todo] = todo
                    it[Todos.done] = done
                }
            }
            return rowToTodo(statement?.resultedValues?.get(0))
        }
    
        // 2
        override suspend fun getTodos(userId: Int): List<Todo> {
            return dbQuery {
                 Todos.select {
                    Todos.userId.eq((userId)) // 3
                }.mapNotNull { rowToTodo(it) }
            }
        }
        // 4
        private fun rowToTodo(row: ResultRow?): Todo? {
            if (row == null) {
                return null
            }
            return Todo(
                id = row[Todos.id],
                userId = row[Todos.userId],
                todo = row[Todos.todo],
                done = row[Todos.done]
            )
        }
    

    Here’s what the code above does:

    1. Defines addTodo, which takes a user ID, the TODO text and the done flag.
    2. Defines the method to get all TODOs for a given user ID.
    3. Note how getTodos uses eq to find a user that matches the user ID.
    4. Defines a helper function to convert an Exposed ResultRow to your TODO class.

    Handling TODOs

    Now, create the routes for handling TODOs.

    Create a new file named TodosRoute in the routes folder and add the following code:

    import com.raywenderlich.API_VERSION
    import io.ktor.application.application
    import io.ktor.application.call
    import io.ktor.application.log
    import io.ktor.auth.authenticate
    import io.ktor.http.HttpStatusCode
    import io.ktor.http.Parameters
    import io.ktor.locations.*
    import io.ktor.request.receive
    import io.ktor.response.respond
    import io.ktor.routing.Route
    import io.ktor.sessions.get
    import io.ktor.sessions.sessions
    import com.raywenderlich.auth.MySession
    import com.raywenderlich.repository.Repository
    
    const val TODOS = "$API_VERSION/todos"
    
    @KtorExperimentalLocationsAPI
    @Location(TODOS)
    class TodoRoute
    

    This defines one route for TODOs. Since you want to create, delete or retrieve TODOs, you can use one route with three different REST methods: POST, DELETE and GET.

    To do so, add the following code below the previous one:

    @KtorExperimentalLocationsAPI
    fun Route.todos(db: Repository) {
        authenticate("jwt") { // 1
            post<TodoRoute> { // 2
                val todosParameters = call.receive<Parameters>()
                val todo = todosParameters["todo"] 
                    ?: return@post call.respond(
                         HttpStatusCode.BadRequest, "Missing Todo")
                val done = todosParameters["done"] ?: "false"
                // 3
                val user = call.sessions.get<MySession>()?.let { 
                    db.findUser(it.userId) 
                }
                if (user == null) {
                    call.respond(
                         HttpStatusCode.BadRequest, "Problems retrieving User")
                    return@post
                }
    
                try {
                    // 4
                    val currentTodo = db.addTodo(
                         user.userId, todo, done.toBoolean())
                    currentTodo?.id?.let {
                        call.respond(HttpStatusCode.OK, currentTodo)
                    }
                } catch (e: Throwable) {
                    application.log.error("Failed to add todo", e)
                    call.respond(HttpStatusCode.BadRequest, "Problems Saving Todo")
                }
            }
        }
    }
    

    This code is similar to the code you used to create a User:

    1. Uses the authenticate extension function to tell the system you want to authenticate these routes.
    2. Defines the new TODO route.
    3. Checks if the user has a session. If not, it returns an error.
    4. Adds the TODO to the database.

      Next, open Application.kt and add the TODO route after the users route in the routing section:

      todos(db)
      

      Testing the Server

      Restart your server and open Postman. Since you restarted the server, you’ll have to log in again.

      Create a new tab to add a new TODO. First, you’ll need to add the authorization token you got when you created the user.

      Go to the Authorization tab and add the jwt_token variable in the token field:

      Postman with the jwt_token in the token field

      Next, go to the Body tab. Create a TODO parameter, or key. The value field is the name of the TODO.

      Set the done state by adding another key named done and setting its value to true.

      Give a value for the TODO and press Send. You’ll see the JSON result in the body section below.

      JSON results in Postman

      To cap things off, in the next section, you’ll create the routes to fetch TODOs.

      Retrieving TODOs

      Now that your users can save their TODOs, they need to be able to call them up again as well.

      Open TodosRoute and add the following underneath the post TodoRoute function:

      get<TodoRoute> {
          val user = call.sessions.get<MySession>()?.let { db.findUser(it.userId) }
          if (user == null) {
              call.respond(HttpStatusCode.BadRequest, "Problems retrieving User")
              return@get
          }
          try {
              val todos = db.getTodos(user.userId)
              call.respond(todos)
          } catch (e: Throwable) {
              application.log.error("Failed to get Todos", e)
              call.respond(HttpStatusCode.BadRequest, "Problems getting Todos")
          }
      }
      

      Test the Server

      Restart your server and open Postman. Log in so you have an updated token, then create a new tab to get your TODOs.

      Add the authorization token you got when you created the user and press Send. You’ll see something like:

      Results in Postman

      Where to Go From Here?

      Congratulations! This was a loooong journey, but you’ve reached your destination. You’ve created a REST Server!

      Download the final version of this project using the Download Materials button at the top or bottom of this tutorial.

      If you want to learn more about Ktor, swing over to ktor.io/, or checkout the video course if you’re interested in learning how to dockerize and deploy your Ktor server to the cloud.

      If you’re interested in learning about how to consume REST APIs from Android, checkout the video course on the site.

      We hope you enjoyed this tutorial. If you have any questions, comments or musings, please join the forum discussion below.

Average Rating

4.8/5

Add a rating for this content

5 ratings

Contributors

Comments