Graphql refresher

TLDR; The following is a quick refresher on GraphQL.

Why GraphQL?

The reliance on endpoints to fetch data for applications been a practice since the early days of web development. With the increasing number of devices that rely on the web and the endpoints, a fit all api becomes complex and hard to provide the best performance to the target device and use-case e.g. a desktop computer with a capabable web browser might show a product page that contains a lot of details which are not important for a mobile experience, or on the go.

There are many solutions for this, such as using query parameters as simple as GET movies?version=mobile, or JSON-API specification e.g. GET movies?include=director&fields[movies]=name,date,rating. There are also query languages created by companies like google, such as the Google Drive API .

All the approaches have tradeoffs regarding customisation, performance, etc. And for development, the hardships of having to compute it e.g. the amout of code required to write server and client side parsing.

Facebook cameout with a new solution after facing many and more of this issues. As put really well by redhat “GraphQL is a query language and server-side runtime for application programming interfaces (APIs) that prioritizes giving clients exactly the data they request and no more”.

GraphQL is a client-centric API

  • Client use cases
  • Easy to use
  • Hard to misuse

✏️ “APIs should be easy to use and hard to misuse” by Joshua Bloch

How does it look like?

Follows a very simple query, where we query a “current” user and the name field. Also, a “query” field tells Graphql that we want to query off the query root of the schema.

query {
  me {
    name
  }
}

Which response,

{
  "data": {
    "me": {
      "name": "Punkbit"
    }
  }
}

The successfull response is always nested under “data” key and the nested data has the same data structure as in the request.

Queries can be more complex, here’s an example where we query the “current” user, the name and first 2 favourite movies.

query {
  me {
    name
    favourite_movies(first: 2) {
      title
      rating
    }
  }
}

As we see, we can traverse complex relationships and that a field can take arguments. Thus, a field is similar to a function, where they take arguments and return a certain type.

{
  "data": {
    name: "Punkbit",
    favourite_movies: [{
      "title": "Pulp fiction",
      "rating": "9.2 out of 10",
    }, {
      "title": "City of god",
      "rating": "9.4 out of 10",
    }]
  }
}

How does it work?

Graphql engine uses a type system that we refer to as the schema, which is represented by the Graphql schema definition language (SDL). The Graphql site offers a guide about the type system and how it describes what data can be queried.

  • What fields can we select?
  • What kinds of objects might they return?
  • What fields are available on those sub-objects?

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#

Object types are the most basic components of a GraphQL schema which is represented by the kind of object you can fetch from a service and what fields it has.

Here’s an example, as provided in the graphql guide

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

Most of it is self explanatory, but note that:

  • Character is a type with with some fields
  • String, built-in scalar type, resolves to a single scalar object and can’t have sub-selections in the query
  • String! means that the field is non-nullable, we use the exclamation point for that
  • [Episode!]! means that the array is non-nullable and will always return an array (zero or more items) and since Episode! is also non-nullable we should expect that every item as an Episode object

Arguments#

Every field in GraphQL object type can have zero or more arguments e.g. as provided in the graphql guide:

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

While a field looks like a function sintatically but unlike languages such as Javascript, where functions takes a list of ordered arguments, all arguments are passed by name specifically.

  • Arguments can be either required or optional
  • When an argument is optional, a default can be set e.g. if unit argument is not passed, it’ll be set to METER by default

💡 Arguments, can be a Scalar type or an Input type. The input types are similar to Scalar types but we declare it using the input keyword.

input MovieChoice {
  title: String!
}

Variables#

Variables can be sent along with the query by the client to the GraphQL API.

query FetchMovie($id: ID!) {
  movie(id: $id) {
    title: String!
  }
}

💡 Notice that we named the query an operation name FetchMovie.

The client would then send the request as:

{
  "id": "Pulp Fiction"
}

Aliases#

The server determines the canonical name of fields and clients may want to receive these fields under a different name. The syntax is simple, as we only have to precede the query field by the alias, as follows:

query {
  userSelectedMovie: movie(id: "Pulp Fiction"): Movie!
}

💡 The client requested a movie field but defined as userSelectedMovie.

Schema Roots#

A GraphQL scheme should always define a Query Root, a type that defines the entry point, usually named Query

type Query {
  movies(id: ID): Movie!
}

The Query type is implicit, whenever we make a request to the GraphQL API. Here’s an example, where we omit the Query field and it implicitly asks for the movies field on the Query root.

{
  movies(id: 1) {
    title
  }
}

💡 A query root has to be defined on a GraphQL schema, but there are two other types of roots that can be defined: Mutation, and Subscription.

Mutations#

An API generally provides endpoints to write and modify data, in GraphQL that’s defined as Mutations.

The entry point to the mutation of a schema is under the Mutation root.

mutation {
  addMovie(title: String!, director: String!, year: String!) {
    movie {
      id
    }
  }  
}

We defined the addMovie mutation as

type Mutation {
  addMovie(title: String!, director: String!, year: String!): AddMoviePayload
}

type AddProductPayload {
  movie: Movie!
}

As we can conclude, mutations are very similar to query fields but there are two important differences:

  • Top-level fields under the mutation root are allowed to have side-effects
  • Top-level mutation fields must be executed serially by the server, while others in parallel

Enumeration types#

Enumeration types or Enums, are a special kind of Scalar type that is restricted to a particular set of allowed values.

As the graphql documentation suggests, this allows you to:

  • Validate that any arguments of this type are one of the allowed values
  • Communicate through the type system that the field will always be one of the finite options
enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

This should be quite familiar conceptualy to what is available in most programming languages. But bare in mind that since GraphQL services can be implemented in any language, these can deal with enums differently. For example, in a language like Javascript these can be mapped to a set of integers since Javascript has no support for enums. However these implementation details don’t leak to the client-side, which operate from the enum values.

Interfaces#

An interface is an abstract type that includes a certain set of fields that a type must include to implement the interface. Here’s a quick example:

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

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

type Droid implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  primaryFunction: String
}

When the types are expected to comply with the contract (Character interface), clients can confidently ask for the fields declared in the contract.

query {
  movies {
    characters {
      friends
      appearsIn
    } 
  }
}

Although, for any other fields that are not in the contract, it must be specified which concrete type by using fragment spreads or typed fragments.

query {
  movies {
    characters {
      friends
      appearsIn
      ... on Human {
        totalCredits
      }
      ... on Droid {
        primaryFunction
      }
    }
  }
}

Union types#

Union types allow us to define an arbitary number of objects that a field can return. Thus they don’t get to specify any common fields between the types.

union SearchResult = Human | Droid | Starship

💡 The members of union types must be concrete object types, we can’t create union types from interfaces or other union types

When query a field that returns an union type, we have to use an inline fragment to be able to query any fields at all.

Here’s an example from the graphql guide:

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

💡 The __typename field resolves to a String which lets you differentiate different data types from each other on the client.

 Fragments#

The inline fragments we used before with the operator ... on Human to select concrete types, allows clients to define parts of a query to be reused elsewhere.

query {
  movies(first: 2) {
    ...MovieFragment
  }
}

fragment MovieFragment on Movie {
  title
  rating
}

We use the fragment keyword followed by a name and the object its applied, in this case Movie.

Find more about it in the graphql guide .

Directives#

Directives provides a way to dynamically change the structure of our queries by using variables.

Here’s an example from the graphql guide

query Hero($episode: Episode, $withFriends: Boolean!) {
  hero(episode: $episode) {
    name
    friends @include(if: $withFriends) {
      name
    }
  }
}

The GraphQL specification provides support for two directives: include and skip

  • @include(if: Boolean), only include a field if the argument is truthy
  • @skip(if: Boolean), skip the field if the argument is truthy

💡 Server implementations can provide custom or experimental directives

Find more about directives in the graphql guide .

Introspection#

GraphQL clients can ask a GraphQL schema about what queries are supported. In GraphQL this is what introspection is used for

Let’s say that if we didn’t designed the type system, we can ask GraphQL about it by querying the __schema field on the root type of a Query.

{
  __schema {
    types {
      name
    }
  }
}

Which response is:

{
  "data": {
    "__schema": {
      "types": [
        {
          "name": "Query"
        },
        {
          "name": "String"
        },
        {
          "name": "ID"
        },
        {
          "name": "Mutation"
        },
        {
          "name": "Episode"
        },
        {
          "name": "Character"
        },
        {
          "name": "Int"
        },
        {
          "name": "LengthUnit"
        },
        {
          "name": "Human"
        },
        ...
      ]
    }
  }
}

Find more about it here!

API Design first#

A developer should be able to use an API easily! The developer should be able to find what and how to achieve it.

The API should guide a developer on best practices and push them away from bad practices.

Here are some notes to help:

  • Design first approach
  • Decouple from system internals (database, language, etc)
  • Work with people with domain knowledge in areas the API will cover e.g. work with teams and people familiar with the exposed use-cases
  • Public API’s are hard to change e.g. breaking changes and support
  • Answer client needs first
  • Share the API design early, provide mock services of the API for quicker feedback
  • Clients should NOT provide the solution, it’s important to look at the problem, gather information, before implementing a solution
  • API’s should provide just enough features for the use-cases we’re interested in
  • Schemas should NOT be influenced by implementation details
  • Availability and performance should be considered although with client in mind
  • System internals should be quicker to modify vs the API which is exposed externally
  • GraphQL vendors whom wrap databases or swagger API’s rarely make sense but can be useful for prototyping
  • Naming is important, a good name should convey information
    • Naming consistency
    • Name things correctly for what they are and do to avoid big deprecations in the long term e.g. you wouldn’t want to expose private user details if the user object is provided in the wrong context of what it was originally created for
  • API symmetry e.g. if we have a addMovie we should have removeMovie (POLA )
  • Describe entities in the schema, these are great as they encode information directly into the schema instead of being found externally like docs
  • Avoid runtime logic when the schema can enforce it
  • Use complex object and input types to represent coupling between fields and arguments: avoid “impossible states”
  • Use default values to indicate what the default behavior is when using optional inputs and arguments
  • On specificity vs generics, fields should often do one thing, and do it really well as GraphQL core philosophy is to let clients consume exactly what they need

According to production ready graphql, there are four main points we should remember when building a GraphQL Schema:

  • First, use a design-first approach to schema development. Discuss design with teammates that know the domain best and ignore implementation details.
  • Second, design in terms of client use cases. Don’t think in terms of data, types, or fields.
  • Third, make your schema as expressive as possible. The schema should guide clients towards good usage. Documentation should be the icing on the cake.
  • Finally, avoid the temptation of a very generic and clever schema. Build specific fields and types that clearly answer client use cases.

GraphQL Servers#

Here’s a basic implementation that uses Apollo server based on express.js

Dependencies

 yarn add apollo-server@^2 graphql@^14.6.0

src/index.js

const { ApolloServer } = require('apollo-server');

const typeDefs = `
  type Query {
    info: String!
  }  
`;

const resolvers = {
  Query: {
    info: () => `The API for hacker news clone`
  }
};

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

server
  .listen()
  .then(({ url }) =>
    console.log(`Server is running on ${url}`)
  );

Open http://localhost:4000 and query

query {
  info
}

In general, a schema-driven or schema-first development is used where:

  • Extend the GraphQL schema definition with a new root field
  • Implement the corresponding resolver functions to the added fields

References:

https://netflixtechblog.com/embracing-the-differences-inside-the-netflix-api-redesign-15fd8b3dc49d?gi=839c24c9ee11

https://en.wikipedia.org/wiki/Principle_of_least_astonishment

https://www.howtographql.com/

comments powered by Disqus