You need a GraphQL server
In most frameworks used for web development, the tools to build a JSON API are already there. You can build a route structure and easily accept some GETs and POSTs, then output a JSON response. Ruby on Rails even has a special project setup switch that dispenses with the usual HTML-rendering goodies and puts a solid basis for APIs in its place. What follows is that a skilled developer using modern tools can whip up a backend in literally minutes.
Not so with GraphQL. While there are server libraries for many languages, you still incur a speed penalty right out of the gate - simply by having to figure out what's right for your ecosystem. On a more personal note, I'm also not fond of the term "server" being used to describe a library that can be introduced to a project.
There's only one endpoint
Over the years we got used to a particular way of thinking about API structure. At the most basic level, we simply followed REST practices. That meant creating several endpoints per logical model in our app. It's a structure that's easy to grasp for both the API authors and consumers. It also produces well-scoped methods in the backend, making reasoning about the code as easy as about the API itself. This structure is also easy to namespace, e.g. for the purposes of API versioning.
GraphQL only employs a single endpoint, commonly
/graphql. Every request to your API will arrive as a POST request at that endpoint, and from there it's the GraphQL server's responsibility to figure out what the client wants and respond appropriately.
At first glance it feels unwieldy: imagine trying to do everything in a JSON API with a single endpoint! However, it soon starts making sense as your API matures and some things are replaced by others. Deprecation in classic APIs is usually done at the namespace level, moving from
v2. GraphQL affords much more control, up to deprecating a single field. Imagine being able to tell a REST API consumer that you don't want them using the
name field and to use
fancy_name instead! It turns out that what felt unwieldy at first is actually one of the best features.
Everything is typed
JSON doesn't really have all that much in the way of typing. There are strings, numbers, arrays, and objects. Beyond that, you're out of luck. By contrast in GraphQL, everything begins and ends with types, as even the root Query and Mutation are just that - types. The GraphQL DSL enforces type checking on both the server and client, preventing all sorts of nasty surprises.
This is very important especially as SPAs are increasingly type checked themselves, be it by using TypeScript or alternatives like Flow. GraphQL makes it easy to introduce complex and compound types on top of the default values and it quickly becomes second nature to developers on both backend and frontend.
The docs are built-in
In a classic JSON API, the documentation can be an afterthought. And even if it's not, there are a lot of methods to choose from. Do we use some scheme like OpenAPI? Do we then convert it to human-readable form with tools like Swagger? Or do we just dump a whole bunch of Markdown files somewhere? Even though this problem has been solved multiple times, it still requires conscious thought and effort from the team - first to build the docs, then to keep them updated and disseminated. It's an even more complex problem when the API has several sections that are only accessible by e.g. certain user roles.
In GraphQL documentation is a first-class citizen as most servers allow for documenting your types and requests in place. Since early 2018 the GraphQL Schema Definition Language has been made a part of the official spec, so there is precisely one way of documenting a GraphQL API. Also since GraphQL allows defining the visibility of certain parts of the graph, those users who shouldn't are automatically prevented from seeing docs for what they cannot access. Having the decision taken care of and clear guidelines has been a great boon for the team.
There are only two types of action
In contrast to HTTP's GET, POST, PUT, PATCH and DELETE there are only two types of action in GraphQL: Queries and Mutations. The main difference is that Mutations can and will alter the state of the system and Queries will only passively read data out.
I'll admit I'm still on the fence about this one. I enjoy HTTP's plethora of verbs for interacting with resources and being able to use the precisely right tool for the job. GraphQL makes it easier to take care of those hairy cases where any one of the HTTP verbs didn't fit exactly, but incurs the penalty of having to think about what a particular mutation will actually affect. A point can also be made that since there isn't really a built-in standard naming convention you'll have to draw up internal style guides or risk building an inconsistent mess.
You pretty much need a client
Every reasonable GraphQL client implements a client-side result caching mechanism, and many more features. Thanks to all this choices hand-rolling a configuration for a GraphQL client at the entry level is a completely stupefying task. When starting out with GraphQL I especially recommend taking a look at Apollo-Boost since it comes with very reasonable defaults.
Client picks the data
We've all been there: we pull out a list of data from the API and it's missing some crucial field about a related model. We then implement a hack involving N+1 requests while grumbling at the backend developers who scurry around quickly adding it in. That usually isn't the case with a well-implemented GraphQL API, since we can just delve into the data as deeply as we please. Need to see the address of a customer on an order in this batch? Not a problem - at least in theory, which nicely leads us to...
Complexity is harder to foresee
When designing GraphQL from the back-end side it can be hard to think about all the depth a client can delve into the graph. There are a lot of ways to instrument and observe the usage of your graph, and after letting your front-end colleagues play around for a while you can start seeing some lengthy queries performing rather imaginative reads on your data store. In a REST API this is easier to control since you can easily tell the scope of data that will be accessed in a single request. Oftentimes this missed complexity can bite you hard once you release to production. A lot of those times it is also not obvious how to escape this hole you've dug for yourselves.
Numbered pages are really hard
This is a gripe that's tongue-in-cheek really. You can definitely feel that GraphQL was designed at and for Facebook by looking at the way it's intended pagination mechanism works. The so-called Connections are basically endless streams of graph edges, navigation on which is done through cursors instead of the more classical pages. While it's easy to see how that fits with a Facebook-style endless feed of posts, if you want a neatly paginated list with the possibility of going to, say, page 42, you're going to have a much harder time. There are of course ways to work around that, but that's what they are - workarounds.
Would we do it again?
With all the gripes and differences listed above you probably think that we're treating GraphQL as an experiment turned sour and went straight back to REST APIs. That's not true. If anything, we're working to use GraphQL more widely in projects throughout the organization. It's a great technology that has made our jobs easier and better. We did, however, initially invest in GraphQL without realizing completely what kind of growing pains we will be going through.
If you think GraphQL might be right for you, I encourage you to take the plunge. Give yourself ample time and room to safely fail, and you'll be reaping the benefits before long!