Skip to content

Instantly share code, notes, and snippets.

@mcmire
Last active August 29, 2019 19:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mcmire/d6fe3a70f359ddb3a3669a327fdf3c92 to your computer and use it in GitHub Desktop.
Save mcmire/d6fe3a70f359ddb3a3669a327fdf3c92 to your computer and use it in GitHub Desktop.
Guide to GraphQL

A Guide to GraphQL

We use GraphQL to implement APIs across our tech stack, particularly as a protocol for communication between a backend app and frontend app.

REST is a mainstay in the API world; its concepts are more or less integrated into Rails, Django, Symfony, and other modern web frameworks. But GraphQL, which was developed and released by Facebook in 2015, offers a fundamentally different approach. There are some concepts which should translate relatively well, but there are others which break with tradition and may at first glance seem strange. Therefore, before reading this guide, we recommend doing a bit of research to familiarize yourself with the concepts and philosophy behind GraphQL. Here are some great resources for doing that:

After you understand the basics of GraphQL, you will probably want to know how it integrates with your language of choice. On the backend side, we are using the graphql gem. You can learn more about how that works here:

On the frontend side, we are using React alongside the Apollo library. You can learn more about how that works here:

Given these basic resources, this guide attempts to:

  • summarize key information,
  • highlight insights or gotchas that may not be immediately obvious from the official documentation, and
  • dive into the details of how we implement and use GraphQL in our apps.

How GraphQL works

Key concepts

  • Unlike a REST API, a GraphQL API doesn't have individual endpoints: you always hit the same URL whenever you make a request to the backend.
  • The data you send to a GraphQL API which contains instructions on what it should do or return is called a document.
  • Within a given document, you will specify an operation to perform. There are two primary kinds of operations: queries and mutations. You can think of queries as GET requests, and mutations as POST, PATCH, PUT or DELETE requests.
  • A GraphQL document is composed of fields. You can think of a field as either a key (that maps to a value in a hash or object) or a function (that takes arguments).
  • Along with the operation type, the top-level field differentiates one request from another. You can think of it as an action in a Rails controller.
  • Furthermore, each field is strongly typed. This is a very important concept in GraphQL, and thinking in types is quite different especially if you are used to working in a language where types do not factor into everyday writing such as for Ruby or JavaScript.
  • All information about types and fields are kept in the schema, which describes all of the possible ways to send a request. The server knows what the schema is (because you defined it, at some point) and it is able to look at that schema to ensure that a given request is correct (i.e., the document refers to fields that exist, all specified types match their fields, all provided data matches those types, etc.).
  • From the backend perspective, the GraphQL Ruby gem is ORM-agnostic: it doesn't tie directly into Rails, and it doesn't care whether you are using ActiveRecord, Sequel, Mongo, Redis, or whatever to store your data.

The anatomy of a query

The simplest valid GraphQL query looks something like this:

{
  person {
    name
  }
}

Here, person is the "top-level" field, and it returns an object which has a name field.

Note that this is shorthand for the slightly longer:

query {
  person {
    name
  }
}

There is also a full syntax which lets you name the query, although we don't use it:

query GetPerson {
  person {
    name
  }
}

A mutation looks like this:

mutation {
  someTopLevelField {
    someField
  }
}

where, again, the full syntax is:

mutation MyCoolMutation {
  someTopLevelField {
    someField
  }
}

While it is not strictly necessary, in a production-grade GraphQL implementation such as ours, all of the "top-level" fields must be specified with a corresponding selection set. That is, you cannot say this, as it will produce an error:

{
  getSomething
}

Writing GraphQL

Generating errors

In the course of executing an operation, a problem may occur. There are three categories of problems:

  • The user provided invalid data (password is too short, name is missing, address is invalid, etc.)
  • The client provided invalid data (given order id is missing or invalid, a string is provided when a number was expected, etc.)
  • The task performed generated an exception (the order could not be cancelled, a NoMethodError was raised because of nil, etc.)

Therefore, when a GraphQL operation responds, it needs to represent these kinds of problems somehow. This section provides some suggestions on how to do this.

User errors

User errors (or, as the Ruby GraphQL guide calls them, mutation errors) are reserved for specific issues that arise in the course of executing a query or mutation following a user interaction that need to be surfaced to the user.

Usually these errors will result from an attempt to save a model and will be a translation of validation errors that have been collected on that model.

If necessary, such errors may also be created manually.

Some quick tips here:

  • The message for each error is intended for developers and not is meant to be used directly by a frontend. Hence, every error that can occur for a given endpoint must have a unique type so that the frontend can look for that and interpret it accordingly. The valid set of types is contained in the UserErrorType type class.
  • The argumentPath is an array of strings that points to a particular argument that was provided. It is acceptable to leave this null if there is no single argument that can be referenced.

Top-level errors

A "top-level" error is an error that lives at the root level of the response data. In a given response, there may be a data key or an errors key. It is these errors that we are referring to when we talk about top-level errors. A response with such an error looks like this:

{
  "data": null,
  "errors": [
    {
      "message": "This is a message",
      "path": null,
      "extensions": {
        "type": "some_type",
        "class": "SomeException",
        "backtrace": [
          ...
        ]
      }
    }
  ]
}

The shape of the errors array is detailed in the GraphQL spec. The GraphQL Ruby docs also have a section on these errors.

There are two ways that top-level errors can be generated:

Client errors

Client errors result from a failure for the frontend to send correct parameters and can be fixed by adjusting the input. For instance, perhaps a string was provided for the value of an argument when that argument has been defined to take an integer.

The GraphQL Ruby gem will usually generate these kinds of errors.

Server errors

Any unrecoverable problem that occurs during the course of execution can be thought of as a server error. Most of these errors result from random exceptions: as all GraphQL requests come through [redacted], we wrap this action in a rescue, so that if an exception is raised it will automatically be represented and reframed as a top-level error. But there are some specific exceptions that can be raised in certain scenarios. You can usually tell if a server error refers to a specific error by inspecting the type of that error (located under extensions).

Status codes

Under REST, HTTP status codes can be used to distinguish different variants of responses in a more succinct way than a English message might. There are a lot of different kinds of status codes and they can be used for a lot of different scenarios. For instance, say that a response returns a status of either 401 or 422. Both statuses indicate that an error was encountered, but if you wanted to handle an "unauthorized" error in a different way than an "invalid request object" error, then the status code could give you this information without having to look at any other data in the response.

However, GraphQL isn't so particular about status codes. All the response really needs to return is either a "successful" status (2xx) or an "unsuccessful" one (4xx or 5xx). This comes down to how errors are handled on the frontend. When you are using Apollo to make a request, even if it is through the Query or Mutation component, if the response status is >= 300 it will intercept that response and funnel the data into a global error handler.

In practice, this means that from a backend perspective if an exception occurs or if there is a top-level error, the status will be 500. Otherwise, the status will always be 200, even if there are user errors.

Running queries

To assist you in running and testing out queries, the Facebook team, in addition to developing GraphQL itself, also developed a UI called GraphiQL (pronounced "graphical").

Using this tool, you can enter a query on the left and the results will appear on the right. This tool is aware of the schema and is able to autocomplete fields as you type them. You can also browse the schema on the right-hand side by clicking on "Docs" and step through the different types for each field and argument.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment