GraphQL

GraphQL

"A query language for your API"

GraphQL

Can be used for an API that can in turn talk to:

  • other APIs
  • databases

GraphQL vs REST

Advantages of REST

  • widely used
  • simpler

Advantages of GraphQL

  • more flexible
  • more efficient

Advantages of GraphQL

Flexibility of GraphQL:

can describe complex tasks - the server-side API does not have to be adjusted for new use cases

Efficiency of GraphQL:

client can request exactly the data it needs in a single request

Video: GraphQL is the better REST

Differences between GraphQL and REST

REST: endpoint (e.g. /todos) and method (e.g. PUT) are part of the API; a separate endpoint is needed for each type of query

GraphQL: single endpoint (e.g. /api), all requests are POST requests

Differences between GraphQL and REST

REST: complex cases require multiple HTTP requests

GraphQL: data are fetched via a single HTTP request

Differences between GraphQL and REST

REST: Each endpoint always returns the same set of object fields

GraphQL: the client can request exactly the objects and fields it needs

Use cases

  • API service: e.g. get a random number between 1 and 100
  • database query: e.g. get all login names of friends of a specific user

Example: random number service

query (GraphQL):

query {
  random(min: 1, max: 100)
}

answer (JSON):

{
  "random": 23
}

Example: get friends of a user

Query:

query {
  user(login: "john") {
    login
    friends {
      login
    }
  }
}

Example: get friends of a user

Response:

{
  "user": {
    "login": "john",
    "friends": [
      { "login": "mike" },
      { "login": "stephanie" }
    ]
  }
}

Definining and consuming a GraphQL API

consuming an API:

  • query (expressed in the GraphQL language)
  • optional query data (in JSON)

defining an API:

  • schema
  • resolver functions

Example: random numbers

Example shows use of a random number API which provides parameters to set the quantity and max value of the random numbers

Example: random numbers

schema definition:

type Query {
  rand(max: Int!, quantity: Int!): [Int]!
}

resolver function (this depends on the library):

(root, args, context) =>
  Array.from({ length: args.quantity }, () =>
    Math.floor(Math.random() * args.max)
  );

Example: random numbers: fixed query

query random {
  rand(max: 10, quantity: 3)
}

Example: random numbers: parametric query

query:

query random($max: Int!, $quantity: Int!) {
  rand(max: $max, quantity: $quantity)
}

query data (JSON):

{
  "max": 10,
  "quantity": 3
}

Try it out

predefined API with posts and users:

https://api.graph.cool/simple/v1/cjmj9v4mk1zs00182rnrzdrai

Resources

GraphQL vs REST: Example

GraphQL vs REST

Scenario:

A social media app in which we can view a list of our friends. Clicking on a friend takes us to their most recent posts

API in REST

GET /users/$myuserid/friends
GET /users/$otheruserid/posts

API in GraphQL

{
  users(id: "myuserid") {
    friends {
      userid
      name
    }
  }
}

API in GraphQL

{
  users(id: "otheruserid") {
    posts {
      date
      title
      body
    }
  }
}

Adding functionality: new post feed

Display new posts of all friends in a feed

Adding functionality: new post feed

In REST we would either have to send multiple requests to retrieve all posts of friends - or we would have to implement a new endpoint in the API:

GET /postsoffriends/$myuserid

Adding functionality: new post feed

In GraphQL we can implement this with just one request and without creating new endpoints:

{
  users(id: "$myuserid") {
    friends {
      posts {
        date
        title
        body
      }
    }
  }
}

GraphQL compared to SQL

GraphQL compared to SQL

query {
  user(login: "my-username") {
    login
    name
  }
}
SELECT login, name
  FROM user
  WHERE login='my-username';

GraphQL compared to SQL

In GraphQL parameters don't have a predefined meaning.

In SQL the clause WHERE login='my-username' always has the same meaning

In GraphQL the meaning of login: "my-username" is up to the implementation on the server

GraphQL compared to SQL

SQL: relationships between tables (joins) are defined in the query

GraphQL: already knows about relationships when the query is issued → simpler queries

GraphQL compared to SQL

query {
  user(login: "my-username") {
    posts {
      title
    }
  }
}
SELECT post.title
  FROM user
  LEFT JOIN post ON user.id = post.userId
  WHERE user.login = 'my-username';

(extra code: LEFT JOIN post ON user.id = post.userId)

GraphQL compared to SQL

OpenCRUD is a more specifc standard that is based on GraphQL. It maps directly to SQL and can be used in place of it.

Example APIs

GraphQL API examples

from https://github.com/APIs-guru/graphql-apis:

  • GitHub (login required)
  • Reddit (GraphQL Hub)
  • GraphQL Pokémon (second entry!)
  • Star Wars
  • SpaceX Land
  • Todo list
  • FakeQL: customizable mock APIs

FakeQL

https://fakeql.com/

template for simple todos on FakeQL:

{
  "todos": [
    { "id": 1, "title": "Go shopping", "completed": true },
    { "id": 49, "title": "Do taxes", "completed": false },
    { "id": 50, "title": "Do dishes", "completed": false }
  ]
}

GraphiQL Explorer

GraphiQL Explorer

GraphiQL: Browser-based explorer for GraphQL APIs

  • inspect query structure / data structure (click "Docs" in the top right)
  • send experimental queries

Simple examples

Simple examples

Star Wars API:

  • Get a list of titles of all Star Wars films in the database
  • Get a list of planets and planet populations from Star Wars
  • Get a list of starships grouped by films they appear in

List of titles of Star Wars films

query getTitles {
  allFilms {
    films {
      title
    }
  }
}

List of planets and planet populations

query getPlanetsWithPopulations {
  allPlanets {
    planets {
      name
      population
    }
  }
}

List of starships grouped by film

query getStarshipsByFilm {
  allFilms {
    films {
      title
      starshipConnection {
        starships {
          name
        }
      }
    }
  }
}

Parametric queries

Query parameters

Example on https://api.spacex.land/graphql/:

{
  launchesUpcoming(limit: 4) {
    launch_date_utc
    mission_name
  }
}
{
  rocket(id: "falcon9") {
    cost_per_launch
    wikipedia
  }
}

Query parameters

Advanced query - all Falcon 9 launches in 2019:

{
  launchesPast(
    find: { launch_year: "2019", rocket_name: "Falcon 9" }
  ) {
    launch_date_utc
    launch_site {
      site_name
      site_name_long
    }
  }
}

Note: The server-side implementation determines the set of supported parameters (e.g. find, id, limit ...)

Required and optional parameters

Required parameters are (usually) marked with a !. Returned attributes that will always be present (like id) are marked in the same way.

Variables

query:

query getLaunchesByYear($year: String!) {
  launchesPast(find: { launch_year: $year }) {
    launch_date_utc
    launch_site {
      site_name
      site_name_long
    }
  }
}

variables:

{
  "year": "2019"
}

Mutations

Mutations

https://todo-mongo-graphql-server.herokuapp.com/

(supports only one query at a time)

Mutations

Command that triggers the server's add action and returns the id of the new TODO

mutation addTodo($title: String!) {
  add(title: $title) {
    id
  }
}
{
  "title": "shopping"
}

Mutations

mutation toggleTodo($id: String!) {
  toggle(id: $id) {
    id
    completed
  }
}

Mutations

mutation addOneAndClearCompleted($title: String!) {
  add(title: $title) {
    id
  }
  clearCompleted {
    id
  }
}

Mutations

Task: write a query that will delete all previous entries and add two new ones

Mutations

mutation reset {
  toggleAll(checked: true) {
    id
  }
  clearCompleted {
    id
  }
  add(title: "get some rest") {
    id
  }
}

Exercises (GitHub)

Exercises

  • Get all "followers of followers" for a specific GitHub account
  • Get the name of a project and number of stars for all GitHub projects of a specific user

Exercises - solutions

query {
  user(login: "marko-knoebl") {
    followers(first: 10) {
      nodes {
        login
        followers(first: 10) {
          nodes {
            login
          }
        }
      }
    }
  }
}

Exercises - solutions

query {
  user(login: "marko-knoebl") {
    id
    email
    repositories(
      first: 100
      orderBy: { field: STARGAZERS, direction: DESC }
    ) {
      nodes {
        name
        stargazers {
          totalCount
        }
      }
    }
  }
}

Data types

Data types

With GraphQL the returned data types are always known to the client.

Data types

available types:

  • Boolean
  • Int: 32-bit int (signed)
  • Float: 64-bit float
  • String: UTF-8 character sequence
  • ID: unique id serialized as a string
  • Object: object with predefined entries
  • List: list composed of specific other types

GraphQL clients

GraphQL clients

  • graphql.js: reference implementation
  • Apollo Client: easy integration with React, Vue, Angular, ...
  • Relay: advanced integration with React

GraphQL from JavaScript

Sending queries to the server

Queries are sent to the server via HTTP POST requests

The payload is a JSON object which has a query string property (this is also true when sending mutations) and optionally a variables property.

Sending queries to the server

We can try this out from the browser console via fetch (we have to be on the same website):

const requestBody = {
  query:
    'mutation addTodo($title: String!) { add(title: $title) { id } }',
  variables: '{"title": "test"}',
};

const requestBodyStr = JSON.stringify(requestBody);

fetch('https://todo-mongo-graphql-server.herokuapp.com', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: requestBodyStr,
}).then(console.log);

Example: reddit API

const queryTemplate = `
{
  reddit {
    subreddit(name: "javascript") {
      newListings(limit: 2) {
        title
      }
    }
  }
}`;

Example: reddit API

fetch('https://www.graphqlhub.com/graphql', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Accept: 'application/json',
  },
  body: JSON.stringify({ query: queryTemplate }),
})
  .then(r => r.json())
  .then(data => console.log('data returned:', data));

Queries - Advanced

Default variable valuess

query getPokemonByName($name: String = "Pikachu") {
  pokemon(name: $name) {
    number
    image
  }
}

Aliases

Task: number of Pikachu and Raichu

Aliases

This cannot be done the way we know:

query getTwo {
  pokemon(name: "Pikachu") {
    number
  }
  pokemon(name: "Raichu") {
    number
  }
}

Aliases

Why does this not work? The result would look like this:

{
  "data": {
    "pokemon": {
      "number": "025"
    },
    "pokemon": {
      "number": "026"
    }
  }
}

Note the duplicate key: pokemon!

Aliases

In order to avoid this problem we use aliases:

query getTwo {
  pokemon1: pokemon(name: "Pikachu") {
    number
  }
  pokemon2: pokemon(name: "Raichu") {
    number
  }
}

Aliases

response:

{
  "data": {
    "pokemon1": {
      "number": "025"
    },
    "pokemon2": {
      "number": "026"
    }
  }
}

Fragments

Task: get the number, maxHP and image of Pikachu and Raichu

Fragments

query getTwo {
  pokemon1: pokemon(name: "Pikachu") {
    ...essentialData
  }
  pokemon2: pokemon(name: "Raichu") {
    ...essentialData
    id
  }
}

fragment essentialData on Pokemon {
  number
  maxHP
  image
}