Ktor and GraphQL: Getting Started

Learn how to create a GraphQL server using Ktor By Enzo Lizama.

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 Repository Pattern

The Repository pattern separates the data access logic and maps it to the business entities in the business logic. The data access logic and business logic layers communicate via interfaces. So it’s time to create the repository. First, create the repository directory. Then, create the IPlayerRepository.kt file. Add:

import com.raywenderlich.kgraphqlfootball.data.players
import com.raywenderlich.kgraphqlfootball.models.Player
import com.raywenderlich.kgraphqlfootball.models.PlayerInput
import com.raywenderlich.kgraphqlfootball.models.Position

interface IPlayerRepository {
  fun createPlayer(player: Player)
  fun deletePlayer(uid: String)
  fun listPlayers() : List<Player>
  fun filterPlayersByPosition(position: Position): List<Player>
  fun filterPlayersByTeam(team: String): List<Player>
  fun updatePlayer(uid: String, playerInput: PlayerInput)
}

This interface defines the functionality of the player repository. This interface can be reused to abstract calls to a database, or another REST API. In this project, you’ll
define the implementation for each method. Add the following below the interface:

class PlayerRepository : IPlayerRepository {
  override fun createPlayer(player: Player) {
    players.add(player)
  }
    
  override fun deletePlayer(uid: String) {
    players.removeIf { it.uid == uid }
  }
    
  override fun listPlayers(): List<Player> {
    return players
  }
    
  override fun filterPlayersByPosition(position: Position):List<Player> {
    return players.filter { it.position == position }
  }
    
  override fun filterPlayersByTeam(team: String): List<Player> {
    return players.filter { it.team == team }
  }
    
  override fun updatePlayer(uid: String, playerInput: PlayerInput) {
    players.find { it.uid == uid }?.apply {
      name = playerInput.name
      position = playerInput.position
      team = playerInput.team
    }
  }
}

For now, you’re working with built-in data from the list of players. So the operations you’re going to implement are going to be from this local list. You now have all the classes needed to expose your data with GraphQL. Well done!

Ktor Client with GraphQL

With the object model and the interface to the model complete, you’ll turn your focus on wrapping the interface for GraphQL. Specifically, you’ll be using KGraphQL to wrap your interface. KGraphQL is a Kotlin implementation of GraphQL. It provides a rich DSL to set up the GraphQL schema. You’ll start by defining the schema.

Creating a Schema

Your GraphQL server uses a schema to describe the shape of your data graph. This schema defines a hierarchy of types with fields that populate from your back-end data stores. The schema also specifies exactly which queries and mutations are available for clients to execute against your data graph. Start by creating the SchemaGraphQL.kt file in the src directory and add:

import com.apurebase.kgraphql.schema.dsl.SchemaBuilder
import com.raywenderlich.kgraphqlfootball.models.Player
import com.raywenderlich.kgraphqlfootball.models.PlayerInput
import com.raywenderlich.kgraphqlfootball.models.Position
import com.raywenderlich.kgraphqlfootball.respository.IPlayerRepository
import com.raywenderlich.kgraphqlfootball.respository.PlayerRepository

fun SchemaBuilder.schemaValue() {
  // 1
  val repository: IPlayerRepository = PlayerRepository()

  // TODO: Queries and mutations will go here ...
    
  // 2
  inputType<PlayerInput>{
    description = "The input of the player without the identifier"
  }
  // 3
  type<Player>{
   description = "Player object with the attributes name, team, position and identifier"
  }
  // 4
  enum<Position>()
}

In the code above, you have to define two steps:

  1. Define the repository for players that you implemented in the previous section.
  2. The inputType works as a object type for input data models on the GraphQL schema.
  3. Registers the Kotlin data classes with the type method to include it in the created schema type system.
  4. Registers the Kotlin enum with the enum method to include it in the created schema type system.

If you restart your server in IntelliJ, and refresh your browser, you should notice a Schema tab to the far right. If you click on it, you should see your models described in the panel shown. You can see it in the screenshot below:

Schema

You’ll notice that any description you fill when describing your model is also shown here. This can be useful for developers who might use your interface to better understand the models provided by your endpoint.

With the data types defined in the schema, the next step will be to add your first query.

Fetching Data with Queries

A GraphQL query simply reads or fetches values from your datastore. These queries help reduce over-fetching of data. Unlike a REST API, GraphQL allows a user to select which fields to fetch from the server. This means smaller queries and less traffic over the network, reducing the response time.

You’ll start by adding a method to query all players by a given position. Add the following inside the SchemaBuilder.schemaValue() function:

 // 1
  query("playersByPosition") {
    // 2
    description = "Retrieve all the players by his position"
    // 3
    resolver { position: Position ->
      try {
        // 4
        repository.filterPlayersByPosition(position)
      } catch (e: Exception) {
        emptyList<Player>()
      }
    }
  }

The above code breaks down as follows:

  1. Define a query with a unique name and add a description for the resolver.
  2. Give the method a useful description so developers better understand its usage.
  3. Use a resolver accepting a position and returning a list of Players. Notice that you’ll call the filterPlayersByPosition you added to the IPlayerRepository interface.

In GraphQL every property needs a resolver. The resolver is the piece of system logic, required to resolve the response graph. Since you defined Position enum type, you’re able to accept it as part of the resolver. The final step is to setup the schema when the application is launched.

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

schema { schemaValue() }

Now, run the application and, on the webpage, tap the Schema tab. You’ll see the following result:

playersByPosition

To test the implementation, try to run the playerByPosition query. In the Query section, add:

query fetchPlayerByPosition($position: Position!){
  playersByPosition(position: $position){
    uid
    name
    position
    team
  }
}

Click the Query variables tab and put the following code that represents the object in the argument field. Here, you’re choosing to search for players who are midfielders.

{
  "position": "MID"
}

Click the middle Play button. When you execute this, you should see the following result from the playground:

Result of executing the query on playground

To flesh out the API, add the following players and playersByTeam queries to retrieve a list of all players, and players by team respectively.

  query("players") {
    description = "Retrieve all players"
    resolver { ->
      try {
        repository.listPlayers()
      } catch (e: Exception) {
        emptyList<Player>()
      }
    }
  }
    
  query("playersByTeam") {
    description = "Retrieve all the players by his team"
    resolver { team: String ->
      try {
        repository.filterPlayersByTeam(team)
      } catch (e: Exception) {
        emptyList<Player>()
      }
    }
  }

Notice how the the players doesn’t require any arguments, where as playersByTeam expects a team name as a string.

With queries complete, you can now add the ability to modify or mutate objects using GraphQL. You’ll do that in the next section.