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}`)