Graduate Program KB

GraphQL

Useful Links


What is GraphQL

  • A query language, just how SQL is a query language
  • Gives clients exactly what they ask for, prevents over-fetching of data
  • Can fetch large amounts of varying data in just a single request

GraphQL History

  • Developed by Facebook
  • Released in 2015
  • Adopted by popular APIs (Facebook, GitHub, Shopify)

Why GraphQL For Us

  • AWS AppSync Workshops
  • Widely used
  • Important to know

GraphQL Query

Example:

query AllFilmsTitles {
  allFilms {
    films {
      title
    }
  }
}
  • GraphQL has a type system, the basic root type here is 'query'
  • Perform a POST with this query, rather than a GET -
  • MDN: "GET requests should not include data", must send the query data in GraphQL
  • With this example, something equivalent can be easily achieved with RESTful APIs

GraphQL Query Response

What is returned:

{
  "data": {
    "allFilms": {
      "films": [
        {
          "title": "A New Hope"
        },
        {
          "title": "The Empire Strikes Back"
        }
        // ...
      ]
    }
  }
}

RESTful APIs Refresher

  • The first line is an example taken from the previous talk on RESTful APIs to remind us of their structure
  • The second line is an equivalent version of the GraphQL API we just demonstrated in RESTful API format
  • As you can still, due to it being a relatively simple query, it is easily replicated in different formats
www.thecocktaildb.com/api/json/v1/1/search.php?s=margarita
www.starwarsapi.com/api/json/v1/1/allFilms/films

GraphQL Complex Queries

Here is an example of a more complex query:

query PlanetDataAndImportantResidents {
  planet(id: "cGxhbmV0czo4") {
    name
    gravity
    population
    filmConnection {
      films {
        title
      }
    }
    residentConnection {
      residents {
        name
      }
    }
  }
  starship(id: "c3RhcnNoaXBzOjEy") {
    name
  }
}
  • This example shows the power of GraphQL
  • You can hit a single endpoint, but query many different things all from within a single request
  • Not only do we query 3 scalar fields under planet, but also two further nested sub-fields through filmConnection and residentConnection
  • At the top level, we also query something completely unrelated to the rest of the data through starship
  • Additionally, you might notice we are passing arguments to both planet and starship, this will be discussed more later

Comparison

  • These are the two equivalent RESTful API GET requests that would need to be performed to receive the same data
  • The first difference is that you would have to hit two different endpoints, one for the planet data and one for the starship
  • The second difference is that this would return all data from each field at these end points
  • In comparison, the GraphQL query would only return the fields we requested (name, gravity, population, title, etc)
www.starwarsapi.com/api/json/v1/1/planet.php?s=cGxhbmV0czo4
www.starwarsapi.com/api/json/v1/1/starship.php?s=c3RhcnNoaXBzOjEy

What is returned from our complex GraphQl Query (some results omitted due to size):

{
  "data": {
    "planet": {
      "name": "Naboo",
      "gravity": "1 standard",
      "population": 4500000000,
      "filmConnection": {
        "films": [
          {
            "title": "Return of the Jedi"
          }
          // ...
        ]
      },
      "residentConnection": {
        "residents": [
          {
            "name": "R2-D2"
          }
          // ...
        ]
      }
    },
    "starship": {
      "name": "X-wing"
    }
  }
}
  • As expected, the query still holds a predictable shape even with more complex queries
  • Notice there is still no over-fetching, we only get exactly what we asked for

Structure of a query

  • Fields
  • Arguments
  • Aliases
  • Fragments
  • Operation Type/Name
  • Variables

Fields

query HeroName {
  hero {
    name
  }
}
  • At its simplest, GraphQl is about asking for specific fields on an object
  • Here the field is name, and it will (presumably) return a string type

Arguments

{
  human(id: "1000") {
    name
    height
  }
}
height(unit: "FOOT")
  • It is obviously useful to be able to pass arguments to a field to query specific things
  • Each field and nested object can have their own set of arguments, preventing the need for multiple API fetches
  • You can pass arguments to scalar fields to perform transformations (as shown in the second example above)
  • Scalar types are the 'leaves' of the query, when the field resolves to actual concrete data

Aliases

{
  hero(episode: EMPIRE) {
    name
  }
  hero(episode: JEDI) {
    name
  }
}
  • This type of query will become problematic as we are querying the same field twice but providing different arguments
{
  empireHero: hero(episode: EMPIRE) {
    name
  }
  jediHero: hero(episode: JEDI) {
    name
  }
}
  • As shown above, we can use aliases to mitigate this short coming, we simply rename them
{
  "data": {
    "empireHero": {
      "name": "Luke Skywalker"
    },
    "jediHero": {
      "name": "R2-D2"
    }
  }
}

Fragments

{
  empireComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  jediComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  appearsIn
  friends {
    name
  }
}
  • Using the same example as before, but this time searching for more fields, it will become tedious to re-write the same fields multiple times, particularly as queries become more complex
  • We can use fragments, which are reusable units
  • We construct a set of fields then use them in queries whenever we need

Operation Type/Name

query HeroNameAndFriends {
  hero {
    name
    friends {
      name
    }
  }
}
  • 'query' is the Operation type
  • Mutation/Subscription are the other types (out of scope for this talk, but provide the ability to create/update data and receive continuous updates respectively)
  • 'HeroNameAndFriends' is the operation name, not as important, but useful for when performing multiple queries

Variables

query HumanNameAndHeight($humanId: ID!) {
  human(id: $humanId) {
    name
    height
  }
}
{
  "humanId": "1"
}
  • Previously we passed arguments inline
  • We have now removed the static value and placed a variable
  • Prefixed with $ and in camelCase form
  • The variable is passed in a separate variable dictionary in JSON format

Comparison

This is how we were doing it before:

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

Basic Apollo Server to Demonstrate Resolvers

import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'

// Schema Definition Language (SDL), this is different to the queries/responses we saw earlier
const typeDefs = `#graphql
    type Grad{
        name: String
        id: ID!
    }
    
    type Query {
    allGrads: [Grad!]!,
    grad(id: ID!): Grad!
    }
`

// Simulated data, would typically be stored elsewhere (like in a database for example)
const grads = [
  {
    name: 'Matt',
    id: '1',
  },
  {
    name: 'Khai',
    id: '2',
  },
  {
    name: 'Dempsey',
    id: '3',
  },
  {
    name: 'James',
    id: '4',
  },
]

// Resolver functions tell the server how to resolve a request from a client, here we have basic resolvers due to the nature of our data, the default resolvers are used internally for the scalar fields, resolver functions take up to 4 arguments as shown in the second resolver below
const resolvers = {
  Query: {
    allGrads: () => grads,
    grad(obj, args, context, info) {
      return grads.find((grad) => grad.id === args.id)
    },
  },
}
// Resolver Arguments:
// obj/parent: Return value of parent resolver
// args: Contains arguments provided for that field
// contextValue: Shared across all resolvers. (Something like authentication information)
// info: Data about operations execution state (field name, path, etc)

// Takes in the schema and the resolvers
const server = new ApolloServer({
  typeDefs,
  resolvers,
  formatError: (error) => ({ message: 'BAD!' }),
})

// Passing apollo server instance to startStandaloneServer creates an express ap, installs apollo as middleware, and prepares for incoming requests
const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
})

console.log(`Server ready at: ${url}`)