Deep Dive Into GraphQL Queries

Publikováno: 13.2.2019

In my last GraphQL post, I talked about the good and bad sides of GraphQL. In this post, we'll take an in-depth look at GraphQ...

Celý článek

In my last GraphQL post, I talked about the good and bad sides of GraphQL. In this post, we'll take an in-depth look at GraphQL like queries to understand them better and demonstrate how best we can leverage their functionality to build better API's with GraphQL. Without further ado, let's get started!

Queries

At the bare minimum, GraphQL is just about asking for specific fields on objects. Hence, we can't successfully talk about queries without talking about Fields. Queries are, therefore, the construct used by the client to request specific fields from the server.

Fields

Given that GraphQL is structured to optimally expose one endpoint for all requests, queries are structured to request for specific fields and the server equally structured to respond with the exact fields being requested. Consider a situation where a client wants to request for soccer players from an API endpoint, the query will be structured like this:

{
    players {
        name
    }
}

This is a typical GraphQL query. Taking a closer look, you'll understand that Queries are structurally made up of two distinct parts,

  1. The root field (players)  -  which is the object containing the payload and
  2. The payload itself (name)  -  which is the field(s) requested by the client.

This is an essential part of GraphQL because the server knows exactly what fields the client is asking for and always responds with that exact data. In the case of our query above, we can have this response:

{
    "players": [
        {"name": "Pogba"},
        {"name": "Lukaku"},
        {"name": "Rashford"},
        {"name": "Marshal"}
    ]
}

The field name returns a String type, in this case, the names of the Manchester United players. However, we are not limited to just Strings, we can have fields of all data types just like the root field players returns an array of items. Feel free to learn more about the GraphQL Type System.

In production, we would want to do more than just returning names, the nest could go deeper than that. For instance, in our last query above, we can redefine the query to select an individual player from the list and query for more data on that player. To be able to do such, we'll need a way to identify that player so we can get his details. In GraphQL, we can achieve this with Arguments. We can't talk about queries and not mention arguments so we might as well get to it now. Yeah, let's kill two birds with one stone here.

Arguments

GraphQL queries allow us to pass in arguments into query fields and nested query objects. Moreso, you can pass arguments to every field and every nested object in your query to further deepen your request and make multiple fetches. Arguments serve the same purpose as your traditional query parameters or URL segments in REST. We can simply pass them into our query fields to further specify how the server should respond to our request.

Back to our earlier situation of fetching a specific player's kit details like shirt size or shoe size, first, we'll have to specify that player by passing in an argument id to identify the player from the list of players and then we'll define the fields we want in the query payload:

{
    player(id : "Pogba") {
        name,
        kit {
            shirtSize,
            bootSize
        }
    }
}

Here, we are requesting the desired fields on the player Pogba because of the id argument we passed into the query. Just like fields, there are no type restrictions, arguments can be of different types too. The result of the query above with the id argument will look like so:

{
    "player": {
        "name": "Pogba",
        "kit": [
            {
            "shirtSize": "large",
            "shoeSize": "medium"             }
         ]

    }
}

One thing we can perceive as a pitfall here is that GraphQL queries look almost the same for both single items and or lists of items. Sometimes it can get a little confusing which one to expect but luckily, we always know what to expect based on what is defined in the schema. One more thing is, GraphQL queries are interactive, you can add more fields to the root field object at will. That way, you, as the client, have the flexibility to avoid round trips and request for as much data as you want in a single request.

That alright. Now, what happens if we want to fetch the same fields for two players, not just one? That's where aliases come in.

Aliases

If you take a closer look at our last example, you'll notice that the result object fields

// result....
"player": {
        "name": "Pogba",
        "kit": [
            {
            "shirtSize": "large",
            "shoeSize": "medium"             }
         ]

    }

match the query fields

//query.... has matching fields with the result
player(id : "Pogba") {
        name,
        kit {
            shirtSize,
            bootSize
        }
    }

but without the argument.

(id : "Pogba")

Because of this, we can't directly query for the same field player with different arguments, i.e we can't do something like this:

{
    player(id : "Pogba") {
        name,
        kit {
            shirtSize,
            bootSize
        }
    } 
    player(id : "Lukaku") {
        name,
        kit {
            shirtSize,
            bootSize
        }
    }

}

We can't do that, what we can do however is use aliases. They let us rename the result of a field to anything we want. For our example, to query for two players kit details, we'll simply define our query like so:

{
  player1: player(id: "Pogba") {
    name,
    kit {
        shirtSize,
        shoeSize
    }
  }
  player2: player(id: "Lukaku") {
    name,
    kit {
        shirtSize,
        shoeSize
    }
  }
}

Here, the two player fields would have conflicted, but since we can alias them to different names player1 and player2, we can get both results in one request like so:


{
  "data": {
    "player1": {
      "name": "Luke Skywalker",
        "kit": [
            {
            "shirtSize": "large",
            "shoeSize": "medium" 
            }
         ]
    },
    "player2": {
      "name": "Lukaku",
        "kit": [
            {
            "shirtSize": "extralarge",
            "shoeSize": "large" 
            } 
        ]
    }
  }
}

Now using Aliases, we have successfully queried the same field with different Arguments and gotten the expected response.

Operation syntax

Until now, we have been using the shorthand operation syntax where we are not explicitly required to define either the operation name or type. In production, the reverse is the case, though not compulsory, it is advised to use operation names and types to help make your codebase less ambiguous. It also helps with debugging your query in the case of an error.

The operation syntax comprises basically of two things:

  • The operation type which could be either query, mutation or subscription. It is used to describe the type of operation you intend to carry out.

  • The operation name which could be anything that will help you relate with the operation you're trying to perform.

Now, we can rewrite our earlier example and add operation type and name like this:

query PlayerDetails{
    player(id : "Pogba") {
        name,
        kit {
            shirtSize,
            bootSize
        }
    }
}

Where: query is the operation type and PlayerDetails is the operation name.

Variables

So far, we have been passing all our arguments directly into the query string. In most cases, the arguments we pass are dynamic. Say for instance, the player the client wants his details comes from a text input form or a dropdown menu. That way, the argument we pass into the query string has to be dynamic and to do that, we need to use variables. What are variables you may ask? variables are basically used to factor out dynamic values from queries and pass them as a separate dictionary.

Considering our last example, if we wanted to make the player dynamic such that the selected player's details are returned, we'll have to store the player's id value in a variable and pass it into the operation name and query argument like so:

query PlayerDetails ($id: String){
    player (id : $id) {
        name,
        kit {
            shirtSize,
            bootSize
        }
    }
}

Here, $title: String is the variable definition and title is the variable name. It is prefixed by $ followed by the type, which in this case is String. This means that we can avoid manually interpolating strings to construct dynamic queries and that is awesome.

Fragments

We've come a long way but we are not done yet. Looking at our query, you'll notice that the player field is practically the same for both players:

        name,
        kit {
            shirtSize,
            bootSize
        }

To be more efficient with our query, we can extract this piece of shared logic into a reusable fragment on the player field like so:

{
  player1: player(id: "Pogba") {     ...playerKit
  }
  player2: player(id: "Lukaku") {
    ...playerKit
  }
}

fragment playerKit on player {
    name,
    kit {
        shirtSize,
        shoeSize
    }
}

The ability to extract a piece of shared code and reuse across multiple fields is a very crucial concept that helps developers not go DRY - (Don't Repeat Yourself) in development and even in production. If you're working on a deep layered codebase, you'll find it rather useful and timely to reuse code rather than repeat your self across multiply components.

Directives

GraphQL Directives provides us a way to tell the server whether to include or skip a particular field in when responding to our query. There are basically two built-in directives in GraphQL that helps us achieve just that:

  1. @skip for skipping a particular field when the value passed into it is true and
  2. @include to include a particular field when the value passed into it is true

Let's add a Boolean directive and skip it on the server with the @skip directive:

query PlayerDetails ($playerShirtDirective: Boolean!){
    player(id: "Pogba") {
        name,
        kit {
            shirtSize @skip(if: $playerShirtDirective)
            bootSize
        }
    }
}

The next thing we'll do is to create the playerShirtDirective directive in our query variables and set it to true:

// Query Variables
{
  "itemIdDirective": true
}

This will now return the payload without the shirtSize :

"player": {
        "name": "Pogba",
        "kit": [
            {
            "shoeSize": "medium"             }
         ]

    }

We can reverse this situation using the @include directive. It works as the opposite of the @skip directive. Needless to show, you can use it to reverse this skip action on the server simply by replacing @skip directive with @include in the query.

Conclusion

In this article, we have gone over the not so popular bits of GraphQL Queries in the simplest way possible. This is in response to the many requests i got to cover Queries after my last GraphQL post. Subsequently, we'll take an in-depth look at other GraphQL features like Mutations and Subscriptions to uncover their unpopular but useful concepts. Until then, Happy New Year.

Nahoru
Tento web používá k poskytování služeb a analýze návštěvnosti soubory cookie. Používáním tohoto webu s tímto souhlasíte. Další informace