GraphQL

The what, the why and the how

What GraphQL is not

A framework

An alternative for SQL

A silver bullet

Mature

What GraphQL is

A query language

An alternative to REST

A great solution to irksome problems

Mature-ish

The Problems

1. Underfetching

GET /movies/?q=Hackers HTTP/1.1
{
  "movies": [{ "id": "1337", "title": "Hackers", "year": "1995" }]
}
GET /movies/1337/reviews HTTP/1.1
{
  "reviews": [
    {
      "id": "1011",
      "rating": 3.5,
      "text": "I didn't get most of the jokes but Angelina Jolie *almost* acts",
      "author": "Dave"
    },
    {
      "id": "1012",
      "rating": 1,
      "text": "I've never seen such heavy-handed product placement",
      "author": "Geoff"
    }
  ]
}
GET /movies/1337/characters HTTP/1.1
GET /movies/1337/images HTTP/1.1

Underfetching

Not enough data in each response

Need to send (many) more requests

Impacts user experience

Impacts developer experience

2. Overfetching

GET /flights/?from=JFK&to=LHR HTTP/1.1

... some time later ...

{
  "flights": [
    {
      "flightId": 271046927,
      "carrier": {
        "fs": "AA",
        "iata": "AA",
        "icao": "AAL",
        "name": "American Airlines",
        "phoneNumber": "1-800-433-7300",
        "active": true
      },
      "flightNumber": "100",
      "departureAirport": {
        "fs": "JFK",
        "iata": "JFK",
        "icao": "KJFK",
        "faa": "JFK",
        "name": "John F. Kennedy International Airport",
        "street1": "JFK Airport",
        "street2": "",
        "city": "New York",
        "cityCode": "NYC",
        "stateCode": "NY",
        "postalCode": "11430",
        "countryCode": "US",
        "countryName": "United States",
        "regionName": "North America",
        "timeZoneRegionName": "America/New_York",
        "weatherZone": "NYZ076",
        "localTime": "2012-08-09T14:47:31.438",
        "utcOffsetHours": -4,
        "latitude": 40.642335,
        "longitude": -73.78817,
        "elevationFeet": 13,
        "classification": 1,
        "active": true,
        "delayIndexUrl": "https://api.flightstats.com/flex/delayindex/rest/v1/json/airports/JFK?codeType=fs",
        "weatherUrl": "https://api.flightstats.com/flex/weather/rest/v1/json/all/JFK?codeType=fs"
      },
      "arrivalAirport": {
        "fs": "LHR",
        "iata": "LHR",
        "icao": "EGLL",
        "name": "Heathrow Airport",
        "city": "London",
        "cityCode": "LON",
        "stateCode": "EN",
        "countryCode": "GB",
        "countryName": "United Kingdom",
        "regionName": "Europe",
        "timeZoneRegionName": "Europe/London",
        "localTime": "2012-08-09T19:47:31.439",
        "utcOffsetHours": 1,
        "latitude": 51.469603,
        "longitude": -0.453566,
        "elevationFeet": 80,
        "classification": 1,
        "active": true,
        "delayIndexUrl": "https://api.flightstats.com/flex/delayindex/rest/v1/json/airports/LHR?codeType=fs",
        "weatherUrl": "https://api.flightstats.com/flex/weather/rest/v1/json/all/LHR?codeType=fs"
      },
      "departureDate": {
        "dateLocal": "2012-08-07T18:10:00.000",
        "dateUtc": "2012-08-07T22:10:00.000Z"
      },
      "arrivalDate": {
        "dateLocal": "2012-08-08T06:20:00.000",
        "dateUtc": "2012-08-08T05:20:00.000Z"
      },
      "status": "L",
      "schedule": {
        "flightType": "J",
        "serviceClasses": "RFJY",
        "restrictions": ""
      },
      "operationalTimes": {
        "publishedDeparture": {
          "dateLocal": "2012-08-07T18:10:00.000",
          "dateUtc": "2012-08-07T22:10:00.000Z"
        },
        "publishedArrival": {
          "dateLocal": "2012-08-08T06:20:00.000",
          "dateUtc": "2012-08-08T05:20:00.000Z"
        },
        "scheduledGateDeparture": {
          "dateLocal": "2012-08-07T18:10:00.000",
          "dateUtc": "2012-08-07T22:10:00.000Z"
        },
        "actualGateDeparture": {
          "dateLocal": "2012-08-07T18:05:00.000",
          "dateUtc": "2012-08-07T22:05:00.000Z"
        },
        "flightPlanPlannedDeparture": {
          "dateLocal": "2012-08-07T18:54:00.000",
          "dateUtc": "2012-08-07T22:54:00.000Z"
        },
        "estimatedRunwayDeparture": {
          "dateLocal": "2012-08-07T18:49:00.000",
          "dateUtc": "2012-08-07T22:49:00.000Z"
        },
        "actualRunwayDeparture": {
          "dateLocal": "2012-08-07T18:23:00.000",
          "dateUtc": "2012-08-07T22:23:00.000Z"
        },
        "scheduledGateArrival": {
          "dateLocal": "2012-08-08T06:20:00.000",
          "dateUtc": "2012-08-08T05:20:00.000Z"
        },
        "estimatedGateArrival": {
          "dateLocal": "2012-08-08T06:07:00.000",
          "dateUtc": "2012-08-08T05:07:00.000Z"
        },
        "actualGateArrival": {
          "dateLocal": "2012-08-08T06:09:00.000",
          "dateUtc": "2012-08-08T05:09:00.000Z"
        },
        "flightPlanPlannedArrival": {
          "dateLocal": "2012-08-08T06:14:00.000",
          "dateUtc": "2012-08-08T05:14:00.000Z"
        },
        "estimatedRunwayArrival": {
          "dateLocal": "2012-08-08T06:07:00.000",
          "dateUtc": "2012-08-08T05:07:00.000Z"
        },
        "actualRunwayArrival": {
          "dateLocal": "2012-08-08T06:05:00.000",
          "dateUtc": "2012-08-08T05:05:00.000Z"
        }
      },
      "codeshares": [
        {
          "carrier": {
            "fs": "LY",
            "iata": "LY",
            "icao": "ELY",
            "name": "El Al",
            "active": true
          },
          "flightNumber": "8051",
          "relationship": "L"
        },
        {
          "carrier": {
            "fs": "IB",
            "iata": "IB",
            "icao": "IBE",
            "name": "Iberia",
            "active": true
          },
          "flightNumber": "4218",
          "relationship": "L"
        },
        {
          "carrier": {
            "fs": "BA",
            "iata": "BA",
            "icao": "BAW",
            "name": "British Airways",
            "phoneNumber": "1-800-AIRWAYS",
            "active": true
          },
          "flightNumber": "1511",
          "relationship": "L"
        },
        {
          "carrier": {
            "fs": "GF",
            "iata": "GF",
            "icao": "GFA",
            "name": "Gulf Air",
            "active": true
          },
          "flightNumber": "6654",
          "relationship": "L"
        }
      ],
      "flightDurations": {
        "scheduledBlockMinutes": 430,
        "blockMinutes": 424,
        "scheduledAirMinutes": 380,
        "airMinutes": 402,
        "scheduledTaxiOutMinutes": 44,
        "taxiOutMinutes": 18,
        "scheduledTaxiInMinutes": 6,
        "taxiInMinutes": 4
      },
      "airportResources": {
        "departureTerminal": "8",
        "departureGate": "B3",
        "arrivalTerminal": "3",
        "arrivalGate": "36"
      },
      "flightEquipment": {
        "scheduledEquipment": {
          "iata": "777",
          "name": "Boeing 777 Passenger",
          "turboProp": false,
          "jet": true,
          "widebody": true,
          "regional": false
        },
        "tailNumber": "N753AN"
      },
      "irregularOperations": [
        {
          "type": "CANCELLATION",
          "dateUtc": "2013-02-26T23:00:00.000Z",
          "message": "Cancelled due to mechanical issue."
        },
        {
          "type": "REPLACED_BY",
          "relatedFlightId": 24586521,
          "dateUtc": "2013-02-26T23:00:00.000Z",
          "message": "New flight created to replace a cancelled flight."
        }
      ],
      "confirmedIncident": {
        "publishedDate": "2013-02-26T23:00:00.000Z",
        "message": "The plan has slid off the runway."
      },
      "operatingCarrier": {
        "fs": "AA",
        "iata": "AA",
        "icao": "AAL",
        "name": "American Airlines",
        "phoneNumber": "1-800-433-7300",
        "active": true
      },
      "primaryCarrier": {
        "fs": "AA",
        "iata": "AA",
        "icao": "AAL",
        "name": "American Airlines",
        "phoneNumber": "1-800-433-7300",
        "active": true
      }
    }
  ]
}

Overfetching

More data than is useful

Hard to explore

Impacts user experience

Impacts developer experience

3. Versioning

GET /v1/movies/1337/reviews HTTP/1.1
GET /v2/movie/1337/review HTTP/1.1
GET /movies/1337/reviews HTTP/1.1

X-API-Version: 2;

Versioning

Hard to manage

Impacts developer experience

Generally a ballache

4. Introspection

The pyramid of REST

Self-describing API

Introspection

Need prior knowledge of API

or, hard-to-consume hypermedia links

5. Modern Applications

Each page requires data from many sources

Building an endpoint for your page is not RESTful

Modern applications are not RESTful

The Solution: GraphQL

GraphQL

Lets you query exactly what data you need

Allows graceful deprecations

Incredible developer experience

Basic Queries

query getMyFavouriteMovies {
  favouriteMovies {
    id
    title
    year
    isFavourite
  }
}
POST /graphql HTTP/1.1
Content-Type: application/graphql

query { movies { id, title, year }}
GET /graphql?query=query+{+movies... HTTP/1.1
query getMyFavouriteMovies {
  favouriteMovies {
    id
    title
    year
    isFavourite
  }
}
{
  "favouriteMovies": [
    {
      "id": "1337",
      "title": "Hackers",
      "year": "1995",
      "isFavourite": true
    }
  ]
}
query getAllMovies {
  movies {
    id
    title
    description
    poster
    reviews {
      id
      text
      author {
        name
        avatar
      }
    }
  }
}

Parameterised Queries

query findMovies(query: String!) {
    searchMovies(query: $query) {
        id
        title
        year
        isFavourite
    }
}
{ "query": "Hackers" }

Mutations

mutation addMovieToFavourites(id: ID!) {
    addMovieToFavourites(id: $id)
}
{ "id": "1337" }

Mutations

mutation reviewMovie(id: ID!, text: String!) {
    addReviewToMovie(movieId: $id, text: $text) {
        id
        text
    }
}
{ "id": "1337", "text": "HACK THE PLANEEEET!" }

Subscriptions

Real-world Usage

Enter: Apollo Server

Disclaimer

Other GraphQL implementations exist

Remember Express.js?

const express = require("express");
const db = require("./database");
const app = express();
const PORT = process.env.PORT || 8080;

app.get("/menu", (req, res) => {
  res.json(db.getAllMenuItems());
});

app.get("/menu/:id", (req, res) => {
  res.json(db.getMenuItem(req.params.id));
});

app.get("/menu/:id/reviews", (req, res) => {
  res.json(db.getMenuItemReviews(req.params.id));
});

app.listen(PORT, () => {
  console.log(`Server listening at http://localhost:${PORT}`);
});

Launching Apollo Server

const { ApolloServer, gql } = require("apollo-server");
const { typeDefs, resolvers } = require("./schema");
const PORT = process.env.PORT || 8080;

const server = new ApolloServer({ typeDefs, resolvers });

server.listen(PORT).then(({ url }) => {
  console.log(`Server listening at ${url}`);
});
const { typeDefs, resolvers } = require("./schema");

Type Definition

Describes the types of data our API works with

Type Definitions

type Movie {
  id: ID!
  title: String!
  year: String!
  isFavourite: Bool
}

type Query {
  allMovies: [Movie]
  movie(id: ID!): Movie
}

Type Definitions

type Movie {
  id: ID!
  title: String!
  year: String!
  isFavourite: Bool
  reviews: [Review]
}

type Review {
  id: ID!
  movie: Movie!
  author: User!
  text: String!
}

type Query {
  allMovies: [Movie]
  movie(id: ID!): Movie
}

Resolvers

Describe how to get data of each type

const resolvers = {
    Query: {
        allMovies() {
          return db.getAllMovies()
        },

        movie(root, args) {
          return db.getMovieById(args.id)
        }
    },
}
```
const resolvers = {
    Query: {
        allMovies: () => db.getAllMovies(),
        movie: (root, args) => db.getMovieById(args.id)
    },

    Movie: {
        reviews: (movie) => db.getReviewsForMovie(movie.id)
    },

    Review: {
        movie: (review) => db.getMovieById(review.movieId),
        author: (review) => db.getUserById(review.authorId)
    }
}
```

Resolvers

function resolver (
    parent,
    args,
    context
) {
    return something;
}

Resolvers

function resolver (
    whereWeCameFrom,
    argumentsFromRequest,
    globalStuffWeSpecify
) {
    return theDataWeRequested;
}
const { ApolloServer, gql } = require("apollo-server");

const typeDefs = gql`
  type Movie {
    id: ID!
    title: String!
    year: String!
  }

  type Query {
    allMovies: [Movie]
  }
`;

const resolvers = {
  Query: {
    allMovies: () => db.getAllMovies()
  }
};

const server = new ApolloServer({ typeDefs, resolvers });

server.listen(8080);

Caveats

Limited type system

Queries can get expensive

Pagination, filtering, sorting

Questions?

const resolvers = {
  Query: {
    contact: {
      email: 'jim@spicy.engineering',
      twitter: 'jimcodes',
      github: 'jimmed',
      phone: '+447̵̜̥̮̬8̬4̭5͈̯̥̻̲̞̼͠3͞1̳6̷̣̙̺08̝̳͕̀6̳'
    },
    slack: ['constructorlabs', 'jimoverflow']
  }
}