REST in Peace – Welcome to GraphQL

Thanks for clicking on this clickbait title 😉. The post is a transcript of my talk about GraphQL I gave at the Open Tech Table Meetup.

The goal was to give everyone who hasn’t used GraphQL so far an introduction and give them reasons to switch from REST APIs. For everyone who is already using GraphQL some tips and lessons that I’ve learned.

What is GraphQL?


GraphQL was developed by Facebook and is a declarative query language. Instead of having multiple HTTP endpoints, as you have in REST APIs, you only have one endpoint where you send your query and request the exact data you need for the client.

Let’s look at the usual REST endpoints you might find in your API

GET /users
POST /users
GET /users/:id
GET /users/todos

However, after some time, you want to optimize and load multiple resource types in one request. You end up not following the REST standard because of your UI choices:

GET /users_with_todos
GET /todos_with_users

In GraphQL however, there is only one Endpoint that you call with a POST Request that contains the query and variables.

POST /graphql
  BODY
    {
      "query": "users { name, todos { text } }",
      "variables": null
    }

I will explain all the details about querying later, but this request would return you the following JSON response:

{
  "data": {
    "users": [
      {
        "name": "Jon Snow",
        "todos": [
          {
            "text": "Get the dragons back"
          },
          {
            "text": "Fight the Night King"
          }
        ]
      }
    ]
  }
}

As you can see, GraphQL is actually no magic – we use POST request for everything and add the query. This also makes it easy to test, as you can see in my blog post about testing GraphQL APIs in Elixir

Now some of you might say - but we have JSON API, which solves all of this as well! The fact that we need an opinionated data structure standard to solve this issue doesn’t seem right to me. By using GraphQL we can design the API as I want to, and GraphQL also solves some issues out of the box.

Core Concepts of GraphQL


Type System

In GraphQL you can define the types that are used within your API. If you have used Flow or Typescript, you are probably very familiar with this style of type definitions. In the following example, we defined User and Todo types, where a user can have multiple todos (defined with [Todo!]!).

type User {
  name: String!
  todos: [Todo!]!
}

type Todo {
  text: String!
  description: String
}

What is interesting in the GraphQL type system is that you need to use ! if you want a field to declare to be not null per default. We will see later why this makes sense, but I can also recommend you to read this post to understand what the costs of having non-null fields are in the end.

Queries

Generally, there are three root level types in our GraphQL system: Query, Mutation and Subscription. These types are used to query data, change data and subscribe to real-time updates.

Let’s go ahead and define a query to receive all users and one to receive specific todo based on the id:

type Query {
  users: [User!]!
  todo(id: ID!) Todo
}

In this case, our users query returns a list of users. However, the list can still be empty (we just defined that the list can not have null values but only entries of the type User). In the todo query you can see that we can pass variables to queries - in this case, the ID of the todo we want to get.

When executing a query on our client, we can give this query a specific name. If we want for example querying all users, we can name the query AllUsersQuery and of course define which fields we want to receive. Since we also defined that each user has multiple todos, we can query them in the users query as well. Here is how this query looks like in action:

query AllUsersQuery {
  users {
    id
    name
    todos {
      id
      text
    }
  }
}

As a result, we get back a JSON with all the data we requested:

{
  "data": {
    "users": [
      {
        "id": "1",
        "name": "Jon Snow",
        "todos": [
          {
            "id": "1",
            "text": "Get the dragons back"
          },
          {
            "id": "2",
            "text": "Fight the Night King"
          }
        ]
      }
    ]
  }
}

Mutations

When we look at typical CRUD applications (Creat Read Update Delete), we are currently only able to read data with queries. For creating, updating and deleting we have the use of the second root level type Mutation.

Let’s define a mutation in our API to add a user and one to update an existing todo.

type Mutation {
  addUser(name: String!): User!
  updateTodo(id: ID!, updatedText: String!): Todo!
}

When executing the mutation, we can again set a name for the mutation and define which fields we want to get in return:

mutation UpdateTodo {
  updateTodo(id: "1", updatedText: "Get the dragons back from the Night King") {
    id
    text
  }
}

So again receive the data back as JSON:

{
  "data": {
    "updateTodo": {
      "id": "1",
      "text": "Get the dragons back from the Night King"
    }
  }
}

I don’t go into the details for Subscription, because I haven’t used them yet in production. However, with subscriptions, you can use graphql on a web socket connection. You can create subscriptions like userAdded or todoUpdated, to which the client can subscribe to over your socket and then gets notified.

Now that we got the basics, I have some practical tips for you:

Practical Tips


How to design mutations

Something I haven’t told you yet: you can only use primary types in the mutation input or define specific input types. A great tip is to use one specific input type and payload for each mutation.

Let’s go back to our addUser(name: String): User mutation and modify it:

mutation addUser(input: AddUserInput!): AddUserPayload

input AddUserInput {
  name: String!
}

type AddUserPayload {
  result: User
}

There are a few advantages when using this schema:

  1. All mutations return the payload on the result field which makes it easier to abstract the logic on your client.
  2. Less typing ;) (you type input instead of every argument in the mutation)
  3. If necessary, you could deprecate input and add a new_input parameter. I don’t recommend it, but at least it is possible.

Treat errors as data

When sending the POST request to our GraphQL API, we either get back the HTTP Status 200 alternatively, if something failed on the server, probably some 5xx status code. In REST, you can return 422 to indicate that there were errors when creating the resources – this is not possible in GraphQL APIs. Instead, we can return validation errors as data.

As you might have seen in our new addUser mutation, the AddUserPayload which we return is optional. As well as the result in the payload which returns the actual User. Let’s modify the mutation payload one more time, and then you see how this helps us:

type AddUserPayload {
  result: User
  errors: [Error!]!
}

type Error {
  field: String!
  message: String!
}

The advantages are now the following: we never expect that we get a payload at all from addUser because the server can always fail. In the payload itself, we also don’t expect to get a user in our result field, because there can always be errors. The errors array should not be used to indicate server errors, but just the usual validation errors we have when validating our data. The benefit is apparently that we now have typed errors!

Lessons learned


Not everything should be a query - Think in Graphs

It’s is extremely helpful when you need to scopes your requests. Here is an example:

{
  todos {
    id
    text
  }
}

What will this query return? All todos of the currently logged in user? All todos of the whole organization? Or all todos of your application?

The scope can be fixed by introducing a query currentUser which returns the currently logged in user from which you can query the todos:

{
  currentUser {
    todos {
      id
      text
    }
  }
}

The GraphQL Schema is not your UI

Okay, this might sound obvious but wasn’t to me in the beginning. I implemented some logic for a Calendar in the Frontend which was rendering all calendar entries in the most efficient way (the least amount of rows). We then had to implement the same logic in our Android and iOS App as well. However, because we didn’t want to implement the same logic for all clients, we thought to implement it on the server. This was a bad idea since we’re not able to cache each result due to requirements.

Get rid of all your REST endpoints

When we started to implement our API, we were not familiar with GraphQL and thought it’s safer to do authentication via REST. In the end, this would not have been necessary. The authentication itself is again just a mutation.

mutation authenticate(username: String!, password: String!): String

The authenticate mutation returns us a token, which we then can use for the rest of our API (add it to the Authentication Header).

Also, when you want to do a file upload - do it over your GraphQL API. Most GraphQL libraries support it out of the box. It just makes sense to have everything typed and also to profit from the automatic reloading features that you get in most GraphQL frontend libraries.

Conclusion


So you’re probably really excited now and want to jump onto your project and remove the REST API and give a talk about GraphQL to your team. However, you haven’t heard the downsides yet!

Okay, I am honest with you there are no downsides! No new technology that gets praised so much ever had downsides and this is the same with GraphQL 😉.

Jokes aside, there are of course a few downsides you should consier:

  • You will quickly run into a N+1 query problem. However, this is already solved by the concept of a DataLoader and there is often a equivalent solution for your library.
  • Caching is a bit trickier since it’s a POST query, it’s just one HTTP endpoint and each query could still be different.
  • Rate liming can be a bit harder on a resource-level than with REST, where you can easily control it per endpoint.

If you have any questions or want me to give my talk about GraphQL at your Meetup/Conference, reach out to me!

Here are also my slides of the talk I gave: