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:
- The Dessert data class provides the attributes for the model. It also overrides the
id
property fromModel
. - 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:
- Initialize a Dessert Repository and define our available data types.
- Create query resolvers that fetch data from the dessert objects.
- 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:
- Challenge: Create
updateDessert
resolver - which modifies an existing dessert with new attributes - 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.