Learning Notes: GraphQL Documentation

  • GraphQL is a query language for your API and a server-side runtime for executing queries using a type system you define for your data.
  • A GraphQL service is created by defining types and fields on those types, then providing functions for each field on each type.
  • GraphQL lets you request certain fields on objects.

After a GraphQL service is running (typically at a URL on a web service), it can receive GraphQL queries to validate and execute. The service first checks a query to ensure it only refers to the types and fields defined and then runs the provided functions to produce a result. - GraphQL Docs

{
  hero {
    name
  }
}

// Will produce
{
  "data": {
    "hero": {
      "name": "R2-D2"
    }
  }
}

Queries and Mutations

Arguments

GraphQL becomes more powerful with the ability to pass arguments to fields.

{
  human(id: "1000") {
    name
    height
  }
}

// Results

{
  "data": {
    "human": {
      "name": "Luke Skywalker",
      "height": 1.72
    }
  }
}
  • One limitation of REST is that you can only have a single set of arguments in the url. With GraphQL, we can have unlimited amounts of arguments, which is equivalent to multiple requests with REST.
{
  human(id: "1000") {
    name
    height(unit: FOOT)
  }
}

// Result
{
  "data": {
    "human": {
      "name": "Luke Skywalker",
      "height": 5.6430448
    }
  }
}

Aliases

We can rename the return object of GraphQL queries to suit our use on the client-side.

{
  empireHero: hero(episode: EMPIRE) {
    name
  }
  jediHero: hero(episode: JEDI) {
    name
  }
}

// Result
{
  "data": {
    "empireHero": {
      "name": "Luke Skywalker"
    },
    "jediHero": {
      "name": "R2-D2"
    }
  }
}
  • This comes in handy when we have multiple fields with the same name.

Fragments

  • GraphQL queries can get repetitive pretty quickly. GraphQL includes reusable units known as Fragments. Here's an example of using Fragments.
{
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  appearsIn
  friends {
    name
  }
}

// Results

{
  "data": {
    "leftComparison": {
      "name": "Luke Skywalker",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ],
      "friends": [
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        },
        {
          "name": "C-3PO"
        },
        {
          "name": "R2-D2"
        }
      ]
    },
    "rightComparison": {
      "name": "R2-D2",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ],
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}
  • We can even use variables in fragments
query HeroComparison($first: Int = 3) {
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  friendsConnection(first: $first) {
    totalCount
    edges {
      node {
        name
      }
    }
  }
}

// Result

{
  "data": {
    "leftComparison": {
      "name": "Luke Skywalker",
      "friendsConnection": {
        "totalCount": 4,
        "edges": [
          {
            "node": {
              "name": "Han Solo"
            }
          },
          {
            "node": {
              "name": "Leia Organa"
            }
          },
          {
            "node": {
              "name": "C-3PO"
            }
          }
        ]
      }
    },
    "rightComparison": {
      "name": "R2-D2",
      "friendsConnection": {
        "totalCount": 3,
        "edges": [
          {
            "node": {
              "name": "Luke Skywalker"
            }
          },
          {
            "node": {
              "name": "Han Solo"
            }
          },
          {
            "node": {
              "name": "Leia Organa"
            }
          }
        ]
      }
    }
  }
}

Operation Names and Types

  • We can make our queries more descriptive by adding operation names and types like so
query HeroNameAndFriends {
  hero {
    name
    friends {
      name
    }
  }
}

// Result
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}
  • The query name can be any string, while the query type can be any of query, mutation or subscription. Having operation names is very helpful for debugging and server-side logging.

Variables

  • GraphQL has support for variables from our queries. One interesting thing to note is how these variables are implemented. They're not basic string interpolations, but actually native variables.

It wouldn't be a good idea to pass these dynamic arguments directly in the query string because then our client-side code would need to dynamically manipulate the query string at runtime, and serialize it into a GraphQL-specific format. Instead, GraphQL has a first-class way to factor dynamic values out of the query, and pass them as a separate dictionary.

  • Here's an example
query HeroNameAndFriends($episode: Episode) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}

// variables
{
  "episode": "JEDI"
}

// Result
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}

Variable definitions can be optional or required. In the case above, since there isn't an! next to the Episode type, it's optional. But if the field you are passing the variable into requires a non-null argument, then the variable has to be required as well.

  • Variables are optional by default. We can also make them compulsory with the ! mark. We can also declare default variables with this syntax.
query HeroNameAndFriends($episode: Episode = JEDI) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}

Directives

  • We can also use directives for more powerful filtering. Here's an example
query Hero($episode: Episode, $withFriends: Boolean!) {
  hero(episode: $episode) {
    name
    friends @include(if: $withFriends) {
      name
    }
  }
}

// variables
{
  "episode": "JEDI",
  "withFriends": true
}

// Result
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}
  • In the query above, we're saying include the results with withFriends as true.
  • One other GraphQL directive is @skip that does the direct opposite.

Mutations

  • There's no concept of GET or POST in GraphQL. Any request can do pretty much anything. But it's a great convention to only use mutations for write requests.
  • We can also return objects after running mutations just like we can do in queries.
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}

// Variables
{
  "ep": "JEDI",
  "review": {
    "stars": 5,
    "commentary": "This is a great movie!"
  }
}

// Results

{
  "data": {
    "createReview": {
      "stars": 5,
      "commentary": "This is a great movie!"
    }
  }
}
  • Mutations can contain multiple fields like queries. The only difference is that mutations occur in series to avoid race conditions

While query fields are executed in parallel, mutation fields run in series, one after the other.

Meta Fields

  • There're situations where you'd want to know the type of resources returned. We can do that with the __typename field.
{
  search(text: "an") {
    __typename
    ... on Human {
      name
    }
    ... on Droid {
      name
    }
    ... on Starship {
      name
    }
  }
}

// Result

{
  "data": {
    "search": [
      {
        "__typename": "Human",
        "name": "Han Solo"
      },
      {
        "__typename": "Human",
        "name": "Leia Organa"
      },
      {
        "__typename": "Starship",
        "name": "TIE Advanced x1"
      }
    ]
  }
}

Schemas and Types

Every GraphQL service defines a set of types that completely describe the set of possible data you can query on that service. Then, when queries come in, they are validated and executed against that schema.

Object Types and fields

  • Object types are the most basic parts of a GraphQL schema. They depict supported types on a GraphQL service.

Every GraphQL service has a query type and may or may not have a mutation type. These types are the same as a regular object type, but they are special because they define the entry point of every GraphQL query. So if you see a query that looks like:

query {
  hero {
    name
  }
  droid(id: "2000") {
    name
  }
}

<!-- Result -->

{
  "data": {
    "hero": {
      "name": "R2-D2"
    },
    "droid": {
      "name": "C-3PO"
    }
  }
}
  • This means the Query service needs to have Query type with hero and droid fields.
type Query {
  hero(episode: Episode): Character
  droid(id: ID!): Droid
}

Scalar Types

  • Take this query for example
query {
  hero {
    name
    appearsIn
  }
}

// Result

{
  "data": {
    "hero": {
      "name": "R2-D2",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ]
    }
  }
}
  • The name and appearsIn field on the hero object all resolve to scalar types. This is because they don't have any subfields. GraphQL has the following types out of the box
    • Int
    • Float
    • String
    • Boolean
    • ID
  • We can define custom types by using the scalar keyword.

Enumeration types

  • Enumeration types are also included in the GraphQL spec. In the example below, the Episode type must be any of the following
enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

Interfaces

Interfaces in GraphQL are straightforward. It might seem hard to wrap your head around the GraphQL type system when you're using a language with a weak type system, but popular libraries either build their own implementation where there's none.

interface Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
}


<!-- Implementation -->

type Human implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  starships: [Starship]
  totalCredits: Int
}

type Droid implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  primaryFunction: String
}

Validation

Schema validations occurr in graphql before a response is checked.

. Each field on each type is backed by a function called the resolver which is provided by the GraphQL server developer. When a field is executed, the corresponding resolver is called to produce the next value.

Roots and Resolvers

  • Possible GraphQL types are registered at the top of the GraphQL schema. The most basic ones are Root type and Query type. Here's an example
Query: {
  human(obj, args, context, info) {
    return context.db.loadHumanByID(args.id).then(
      userData => new Human(userData)
    )
  }
}
  • Description of the parameters are as follows
    • obj: The previous object, which for a field on the root Query type is often not used
    • args: The arguments provided to the field in the GraphQL query.
    • context: A value that is provided to every resolver and holds important contextual information like the currently logged in user, or access to a database.
    • info: A value that holds field-specific information relevant to the current query as well as the schema details, also refer to type GraphQLResolveInfo for more details.

GraphQL Introspection

  • We can use the introspection feature of GraphQL to make inquiries on the available object types.
{
  __schema {
    types {
      name
    }
  }
}

// Results in

{
  "data": {
    "__schema": {
      "types": [
        {
          "name": "Query"
        },
        {
          "name": "String"
        },
        {
          "name": "ID"
        },
        {
          "name": "Mutation"
        },
        {...}
      ]
    }
  }
}
  • This query has just listed all types available on our GraphQL service. There are the default ones like String, Boolean, Schema, Type, TypeKind, Field, InputValue, EnumValue, __Directive and there are the custom ones like Query, Character, Human, Episode, Droid.

  • We can get more specific information about these types.

{
  __type(name: "Character") {
    name
    kind
  }
}

// Result

{
  "data": {
    "__type": {
      "name": "Character",
      "kind": "INTERFACE"
    }
  }
}

GraphQL Best Practices

  • HTTP
  • JSON (with GZIP)
  • Versioning
  • Nullability
  • Pagination
  • Server-side Batching & Caching#