Chapter 1: Ktor and KGraphQL

Kotlin Multiplatform Mobile

link Ktor and KGraphQL

Introduction

Ktor is a framework for easily building connected applications – web applications, HTTP services, mobile and browser applications. Modern connected applications need to be asynchronous to provide the best experience to users, and Kotlin coroutines provide awesome facilities to do it in an easy and straightforward way.

Ktor can be used to create a variety of server (and client-side) applications. Whether we want to create a website that serves static and dynamic pages, an HTTP endpoint, a RESTful system, or even microservices, Ktor makes it all possible.

link Installation

Download the starter project by running the following:

git clone --branch start https://github.com/Maelstroms38/ktor-graphql.git

You will need IntelliJ IDEA with the Ktor plugin installed. Then, open the starter project and wait until the IDE finishes loading the project dependencies and indexing the project. Now, you have a fully functional GraphQL server.

Start your server by pressing the green play icon beside the main function. Test to make sure your server is running by going to localhost:8080/graphql. You should see the GraphQL playground in your browser.

With your project up and running, our first step is to set up the object model class before exposing the GraphQL models.

link Model Interface

We can save some time by creating a "base class" interface, that includes an id property. This will be useful for when we implement an interface for creating, reading, updating and deleting models. The CRUD interface will be called a RepositoryInterface.

Before the repository, we can create a model interface to handle getting a Model and it's id property.

Start by creating a models folder in the src directory. Then create the Model.kt interface and insert the following:

interface Model {
    val id: String
}

This interface represents a base class. We can inherit from this class and continue to reference it's id property in the following models: Dessert, Review and User.

link Dessert Model

Create the Dessert.kt file in models directory and insert the following:

package com.example.models

// 1
data class Dessert(override val id: String, var name: String, var description: String, var imageUrl: String): Model

// 2
data class DessertInput(val name: String, val description: String, val imageUrl: String)

Here's what the above code defines:

  1. The Dessert data class provides the attributes for the model. It also overrides the id property from Model.
  2. The DessertInput is the model that we are going to send when we want to modify the data source.

link Add Database

After you define the object model, it's time to create the built-in data that simulates a database. Create a folder named data in the src directory. Then, create a file named database.kt to store the list of desserts. Add the following code:

import com.example.models.Dessert

val desserts = mutableListOf(
     Dessert("1", "Chocolate Chip Cookies", "Gooey", ""),
     Dessert("2", "Ice Cream Cake", "Sweet", ""),
     Dessert("3", "Apple Pie", "Tart", ""),

Working with an in-memory database will allow us to make tweaks early on, but we will soon replace this with a database powered by MongoDB Atlas. Next, let's create a generic interface for operating with our database models.

link Repository Interface

First, create the repository directory. Then, create the RepositoryInterface.kt file. Add:

interface RepositoryInterface<T> {
    fun getById(id: String): T
    fun getAll(): List<T>
    fun delete(id: String): Boolean
    fun add(entry: T): T
    fun update(entry: T): T
} 

This interface defines the functionality of all repositories. Each repository operates on a unique model, which is passed in using generics <T>.

link Dessert Repository

Now that we have defined a RepositoryInterface, we will implement it using our first repository. Create a file named DessertRepository.kt inside the repository directory and add:

package com.example.repository

import com.example.data.desserts
import com.example.models.Dessert
import kotlin.Exception

class DessertRepository : RepositoryInterface<Dessert> {
    override fun getById(id: String): Dessert {
        return try {
            desserts.find { it.id == id } ?: throw Exception("No dessert with that ID exists")
        } catch(e: Throwable) {
            throw Exception("Cannot find dessert")
        }
    }

    override fun getAll(): List<Dessert> {
        return desserts
    }

    override fun delete(id: String): Boolean {
        return try {
            val dessert = desserts.find { it.id == id } ?: throw Exception("No dessert with that ID exists")
            desserts.remove(dessert)
            true
        } catch(e: Throwable) {
            throw Exception("Cannot find dessert")
        }
    }

    override fun add(entry: Dessert): Dessert {
        desserts.add(entry)
        return entry
    }

    override fun update(entry: Dessert): Dessert {
        return try {
            val dessert = desserts.find { it.id == entry.id }?.apply {
                name = entry.name
                description = entry.description
                imageUrl = entry.imageUrl
            } ?: throw Exception("No dessert with that ID exists")
            dessert
        } catch(e: Throwable) {
            throw Exception("Cannot find dessert")
        }
    }
}

The above code is responsible for creating, reading, updating and destroying our dessert models. You may notice that each method in the DessertRepository mirrors our RepositoryInterface - only this time we specify the Dessert model as a return type.

Scope Functions

Kotlin provides a number of useful functions called scope functions. Scope functions can execute a block of code within the context of an object. One example of this is in the update method, where the apply scope function is used to update an existing dessert with new attributes.

Another useful scope function is called let, which we will use throughout the course.

For more information on scope functions, visit the kotlinlang.org.

Let's move on to defining our Dessert Schema.

link Dessert Schema

When working with GraphQL, we must define a schema. The schema serves as entry point, or map of accessible information for our backend service. To define what is available for our backend service, let's create our first schema definition.

In GraphQL, every accessible property requires a resolver. The resolver is a function, required to resolve and return the response data. We use resolvers to configure our schema definition's behavior.

For more information on the GraphQL Schema, visit apollographql.com

Create a new directory called graphql. Inside the graphql directory, create a file named DessertSchema.kt and add the following:

import com.apurebase.kgraphql.schema.dsl.SchemaBuilder
import com.example.models.Dessert
import com.example.models.DessertInput
import com.example.repository.DessertRepository

fun SchemaBuilder.dessertSchema() {

    val repository = DessertRepository()

    // 1
    inputType<DessertInput> {
        description = "The input of the dessert without the identifier"
    }

    type<Dessert> {
        description = "Dessert object with attributes name, description and imageUrl"
    }

    // 2
    query("dessert") {
        resolver { dessertId: String ->
            try {
                repository.getById(dessertId)
            } catch (e: Exception) {
                null
            }
        }
    }

    query("desserts") {
        resolver { ->
            try {
                repository.getAll()
            } catch (e: Exception) {
                emptyList<Dessert>()
            }
        }
    }

    // 3
    mutation("createDessert") {
        description = "Create a new dessert"
        resolver { dessertInput: DessertInput ->
            try {
                val uid = java.util.UUID.randomUUID().toString()
                val dessert = Dessert(uid, dessertInput.name, dessertInput.description, dessertInput.imageUrl)
                repository.add(dessert)
                dessert
            } catch (e: Exception) {
                null
            }
        }
    }
}

The above code defines the following:

  1. Initialize a Dessert Repository and define our available data types.
  2. Create query resolvers that fetch data from the dessert objects.
  3. Create mutation resolvers that send data when we want to modify the dessert objects.

Each resolver requires a unique name and a return value. You may notice that these resolvers do not look like the previous Kotlin functions we wrote. In KGraphQL, each resolver clause accepts a kotlin function and returns some data.

Now that we defined our GraphQL schema, let's install it and test it's functionality.

link Install Dessert Schema

Open up Application.kt and replace the "Hello, world" query with:

import com.example.graphql.dessertSchema

// ...

schema {
    dessertSchema()
}

Test Queries

Start your server by pressing the green play icon beside the main function.

Open the playground in your browser and insert the following to fetch all desserts:

query {
  desserts {
    id
    name
    description
    imageUrl
  }
}

Fetch a single dessert:

query {
  dessert(dessertId: "1") {
    id
    name
    description
    imageUrl
  }
}

Test Mutations

Create a new dessert:

mutation CreateDessert($dessertInput:DessertInput!) {
  createDessert(dessertInput: $dessertInput) {
    id
    name
    description
    imageUrl
  }
}

Inside the Query variables, put the following code that represents the object in the argument field:

{
  "dessertInput": {"name": "Black and White Cookies", "description": "Delicious", "imageUrl": ""}
}

Make sure everything worked as expected, and you are good to go. Well done!

Ready for a challenge?

link Code Lab - Schema Updates

At the end of each section, we will present an exercise to help you review new skills.

Update and Delete Desserts

In this code lab, we ask that you implement the updateDessert and deleteDessert resolvers inside DessertSchema.kt.

Keep in mind that your two new resolvers should perform the following operations:

  1. Challenge: Create updateDessert resolver - which modifies an existing dessert with new attributes
  2. Challenge: Create deleteDessert resolver - which removes an existing dessert with a specified ID

Try this out on your own before peeking at the solution code.

Solution Code
mutation("updateDessert") {
    resolver { dessertId: String, dessertInput: DessertInput ->
        try {
            val dessert = Dessert(dessertId, dessertInput.name, dessertInput.description, dessertInput.imageUrl)
            repository.update(dessert)
            dessert
        } catch (e: Exception) {
            null
        }
    }
}

mutation("deleteDessert") {
    resolver { dessertId: String ->
        try {
            repository.delete(dessertId)
            true
        } catch (e: Exception) {
            null
        }
    }
}

Up next, we will introduce Service Oriented Architecture for our backend project.

We will also introduce Dependency Injection using Koin and KMongo, a Kotlin toolkit for Mongo.

link References

format_list_bulleted
help_outline