GraphQL

GraphQL is a declarative API query language that you can use to fetch data from a server. Facebook started its development in 2012 and open sourced it in 2015.

Why GraphQL?

GraphQL is often considered to be a better alternative to REST, because…

  • it gives you exactly the data you asked for (no overfetching of data) and thus reduces the required amount of data being sent over the wire
  • You can avoid multiple REST calls. GraphQL contains all requested data in a single response.
  • GraphQL does not need versioning because you can extend your API in a backwards compatible way
  • Easy to learn and understand. Queries look very similar to the responses.
  • You can wrap GraphQL around existing APIs to create a single API interface
  • is language agnostic. You can use many languages to build a GraphQL server (yet in this article we will use JavaScript).

Basically it works like this: From your client app you send special GraphQL requests (using a client library for ease of use) to a single /graphql endpoint which allows you to retrieve and manipulate data on the back-end. On the back-end runs a GraphQL service that was composed of Schemas and Types – those are the objects that you want to fetch or manipulate.

Querying data

A GraphQL query might look like this:

{
  hero {
    name
  }
}

The returned data:

{
  "data": {
    "hero": {
      "name": "R2-D2"
    }
  }
}

Fetching related data

The query can also fetch related data using sub-selections. In the following example an array of friends is returned:

{
  hero {
    name
    # Queries can have comments!
    friends {
      name
    }
  }
}
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}

Using arguments

{
  human(id: "1000") {
    name
    height(unit: FOOT)
  }
}
{
  "data": {
    "human": {
      "name": "Luke Skywalker",
      "height": 5.6430448
    }
  }
}

Here we passed “1000” as a fixed string. In a real app this should be dynamic of course. See below on how to use argument variables.

Aliases

Aliases let you rename the result of a field to anything you want. You need this to prevent a response name conflict.

{
  empireHero: hero(episode: EMPIRE) {
    name
  }
  jediHero: hero(episode: JEDI) {
    name
  }
}
{
  "data": {
    "empireHero": {
      "name": "Luke Skywalker"
    },
    "jediHero": {
      "name": "R2-D2"
    }
  }
}

Fragments

Instead of repeating fields in your query over and over you should use Fragments instead:

{
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

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

You can even use variables inside 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
      }
    }
  }
}

Operation name and type

In production apps it’s useful to use operation name and type explicitly to make debugging and server-side logging easier. query is the operation type and HeroNameAndFriends is the operation name:

query HeroNameAndFriends {
  hero {
    name
    friends {
      name
    }
  }
}

The operation type is either query, mutation, or subscription. Only for queries it is allowed to omit specifying operation type and name (query shorthand syntax).

Argument variables

In your app you need to pass dynamic variables to your GraphQL queries instead of hard-coding a value of “1000” for example. So first you write your query specifying your variable names:

query HeroNameAndFriends($episode: Episode = JEDI) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}

Second, you specify your variable values separately:

{
  "episode": "NEWHOPE"
}

All declared variables must be either scalars, enums, or input object types.

Dynamically changing query structure (Directives)

Directives @include and @skip allow you to include or skip entire query structures based on a boolean value:

query Hero($episode: Episode, $withFriends: Boolean!) {
  hero(episode: $episode) {
    name
    # also @skip(if: Boolean)
    friends @include(if: $withFriends) {
      name
    }
  }
}
{
  "episode": "JEDI",
  "withFriends": false
}

Mutations

Creating or updating data is done via Mutations. We can mutate and query the new value of the field with one request.

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}
{
  "ep": "JEDI",
  "review": {
    "stars": 5,
    "commentary": "This is a great movie!"
  }
}

In this example, the review variable we passed in is not a scalar. It’s an input object type.

If a mutation contains multiple fields, then they will be executed one after the other, not parallel. In other words, the first is guaranteed to finish before the second begins, ensuring that we don’t end up with a race condition.

Inline Fragments

Depending on a passed in query argument a query can return fields from different types. Example: If $ep is “JEDI” then the field name of type Droid will be returned, otherwise the field height of type Human is returned. We specify this using Inline Fragments:

query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Droid {
      primaryFunction
    }
    ... on Human {
      height
    }
  }
}

Meta fields

In your client you can check the returned __typename meta field to determine which type was returned:

{
  search(text: "an") {
    __typename
    ... on Human {
      name
    }
    ... on Droid {
      name
    }
    ... on Starship {
      name
    }
  }
}
{
  "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 which 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

type Character {
  name: String!
  appearsIn: [Episode!]!
}

Character is a GraphQL Object Type. name and appearsIn are fields. String is one of the built-in scalar types. String! means that the field is non-nullable (query will always return a value). [Episode!]! represents an non-nullable array of Episode objects (query will always return an array with zero or more items).

Arguments

Here we specify an argument unit with default value METER.

type Starship {
  id: ID!
  name: String!
  length(unit: LengthUnit = METER): Float
}

Query and Mutation types

Every GraphQL service has a query type and may or may not have a mutation type. If there is a front-end query like this:

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

then it means that the GraphQL service needs to have a Query type with hero and droid fields:

type Query {
  hero(episode: Episode): Character
  droid(id: ID!): Droid
}

and a type like:

type Character {
  name: String!
  appearsIn: [Episode]!
}

Scalar types

Scalar fields are fields that contain concrete data, they represent the leaves of the query.

{
  hero {
    name
    appearsIn
  }
}

The scalar type of name is String. The type of appearsIn is [Episode!].

{
  "data": {
    "hero": {
      "name": "R2-D2",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ]
    }
  }
}

GraphQL comes with a set of default scalar types: Int, Float, String, Boolean, ID. We can also define a custom scalar type:

scalar Date

Then it’s up to our implementation to define how that type should be serialized, deserialized, and validated.

Enumerations

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

This means that wherever we use the type Episode in our schema, we expect it to be exactly one of NEWHOPE, EMPIRE, or JEDI.

Lists and Non-Null

This following example means that the list itself can be null, but it can’t have any null members

myField: [String!]
myField: null // valid
myField: [] // valid
myField: ['a', 'b'] // valid
myField: ['a', null, 'b'] // error

The following example means that the list itself cannot be null, but it can contain null values:

myField: [String]!
myField: null // error
myField: [] // valid
myField: ['a', 'b'] // valid
myField: ['a', null, 'b'] // valid

Interfaces

This means that any type that implements Character needs to have these exact fields, with these arguments and return types

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

The following type implements the interface and additionally specifies own fields.

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

Union types

Union types are very similar to interfaces, but they don’t get to specify any common fields between the types.

union SearchResult = Human | Droid | Starship

Wherever we return a SearchResult type in our schema, we might get a Human, a Droid, or a Starship

That’s why you need to use a conditional fragment in queries to be able to query any fields at all:

{
  search(text: "an") {
    __typename
    ... on Human {
      name
      height
    }
    ... on Droid {
      name
      primaryFunction
    }
    ... on Starship {
      name
      length
    }
  }
}

Also, in this case, since Human and Droid share a common interface (Character), you can query their common fields in one place rather than having to repeat the same fields across multiple types:

{
  search(text: "an") {
    __typename
    ... on Character {
      name
    }
    ... on Human {
      height
    }
    ... on Droid {
      primaryFunction
    }
    ... on Starship {
      name
      length
    }
  }
}

Note that name is still specified on Starship because otherwise it wouldn’t show up in the results given that Starship is not a Character .

Input types

Allow you to pass in objects as arguments instead of just scalar types.

In this example we want to pass in a review object…

input ReviewInput {
  stars: Int!
  commentary: String
}

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}

having stars and commentary as properties.

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

Since it is an object that we want to pass we define a input type:

Execution

You can think of each field in a GraphQL query as a function or method of the previous type which returns the next type.

{
  human(id: 1002) {
    name
    appearsIn
    starships {
      name
    }
  }
}

When a field is executed, the corresponding resolver is called to produce the next value. If a field produces a scalar value like a string or number, then the execution completes. However if a field produces an object value then the query will contain another selection of fields which apply to that object. This continues until scalar values are reached. GraphQL queries always end at scalar values.

Root fields & resolvers

At the top level of every GraphQL server is a type that represents all of the possible entry points into the GraphQL API, it’s often called the Root type or the Query type.

In this example, our Query type provides a field called human which accepts the argument id. The resolver function for this field likely accesses a database and then constructs and returns a Human object.

Query: {
  human(obj, args, context, info) {
    return context.db.loadHumanByID(args.id).then(
      userData => new Human(userData)
    )
  }
}

The function containing obj, args, context and info is the resolver function. obj contains the previous object, args contains arguments provided to the field in the GraphQL query, context contains information such as logged in user or access to database and info holds field-specific information relevant to the current query as well as the schema details.

During execution, GraphQL will wait for Promises, Futures, and Tasks to complete before continuing and will do so with optimal concurrency.

Example using Express-GraphQL

yarn add graphql express-graphql

That’s what you request:

{
  human(id: 1002) {
    name
    appearsIn
    starships {
      name
    }
  }
}

That’s the GraphQL service implemented as a NodeJS Express Server. We wait 2 seconds before the response is sent:

var express = require('express');
var graphqlHTTP = require('express-graphql');
var {buildSchema} = require('graphql');

var schema = buildSchema(`
    type Query {
      human(id: ID!): Human
    }
    
    type Human {
      name: String
      appearsIn: [Episode]
      starships: [Starship]
    }
    
    enum Episode {
      NEWHOPE
      EMPIRE
      JEDI
    }
    
    type Starship {
      name: String
    }
`);

var root = {
    human(args, context, info) {
        console.log(`Requesting human with id ${args.id}`)
        return new Promise((resolve, reject) => {
            const userData = {
                name: "Luke",
                appearsIn: ["NEWHOPE"],
                starships: [{name: "Falcon"}]
            };
            setTimeout(() => resolve(userData), 2000);
        });
        // Usually you would ask a database
        // return context.db.loadHumanByID(args.id).then(userData => new Human(userData);
    }
};

var app = express();
app.use('/graphql', graphqlHTTP({
    schema: schema,
    rootValue: root,
    graphiql: true,
}));
app.listen(4000, () => console.log('Now browse to localhost:4000/graphql'));

That’s the response

{
  "data": {
    "human": {
      "name": "Luke",
      "appearsIn": [
        "NEWHOPE"
      ],
      "starships": [
        {
          "name": "Falcon"
        }
      ]
    }
  }
}

About Author

Mathias Bothe Contact me

I am Mathias, born 38 years ago in Heidelberg, Germany. Today I am living in Munich and Stockholm. I am a passionate IT freelancer with more than 14 years experience in programming, especially in developing web based applications for companies that range from small startups to the big players out there. I am founder of bosy.com, creator of the security service platform BosyProtect© and initiator of several other software projects.