Hi, I am Nicolas

I'm a software engineer who fell in love with UI and UX design. After years of freelancing, I am currently working for PSPDFKit and build industry leading frameworks.

← Back to see all posts

REST in Peace – Welcome to GraphQL

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

The goal of my talk was to give everyone who hasn’t used GraphQL so far an introduction and give them reasons to switch from REST APIs. However, also 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 endpoints, as you have in REST APIs, you only have one endpoint where you send your query and request the exact data structure you want.

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

1
2
3
4
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:

1
2
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.

1
2
3
4
5
6
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  "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 standard to solve this issue doesn’t seem right to me. I instead use GraphQL and can design the API as I want, than trying to fit everything into a standard to solve issues GraphQL solves out of the box because of its nature.

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, where a user can have multiple todos (defined with [Todo!]!).

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
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:

1
2
3
4
5
6
7
8
9
10
query AllUsersQuery {
  users {
    id
    name
    todos {
      id
      text
    }
  }
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
  "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.

1
2
3
4
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:

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

So again receive the data back as JSON:

1
2
3
4
5
6
7
8
{
  "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 your 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:

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
5
6
{
  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:

1
2
3
4
5
6
7
8
{
  currentUser {
    todos {
      id
      text
    }
  }
}

The GraphQL Schema is not your UI

Okay, this might sound obvious but wasn’t to us in the beginning. I implemented some logic in our Calendar in the JS Frontend which was rendering all calendar entries in the most optimal way (the least amount of rows). We then had to implement the same logic in our Android and iOS App. 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 you can’t do caching.

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.

1
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 server libraries support it (although the API is probably different), but it makes sense, in the end, to have everything typed and also to profit from the automatic reloading features that you get in your GraphQL client.

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 😉.

The only downside I came across so far is that you can quickly get N+1 queries if you don’t pay attention to the problem. However, this is already solved by DataLoader. So, it’s a solved problem, and I don’t see that much downside, considering all the benefits of GraphQL.

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: