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:
- "From REST to GraphQL" (Marc-Andre Giroux, Full Stack Fest 2016)
- "Data fetching for React applications at Facebook" (Dan Schafer and Jing Chen, React.js Conf 2015)
- "GraphQL at Facebook" (Dan Schafer, react-europe 2016)
- The official GraphQL guide
- The official GraphQL spec
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:
- "The GraphQL Way: A new path for JSON APIs" (Nick Quaranto, RailsConf 2018)
- The official graphql-ruby docs
On the frontend side, we are using React alongside the Apollo library. You can learn more about how that works here:
- "Apollo + GraphQL + React: thinking in queries" (James Baxley, Apollo Day SF, 2018)
- "How to GraphQL" tutorial on using React with Apollo
- The official Apollo Client docs
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.
- 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 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
}
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 (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.
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 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.
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
).
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.
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.