20 March 2018
GraphQL will not solve your API design problems
GraphQL is a query language that seeks to provide a more flexible alternative to REST. It allows an API to support constantly evolving requirements by letting consumers shape the responses returned by the server.
Where REST is organised into separate resources, GraphQL offers a single end-point that exposes a schema and query language. Data is defined using an abstraction based on entities, queries and mutations, though you have complete freedom over how this can map onto the underlying data.
The query language has its limitations, but it is powerful enough to shape responses from complex data schemas. This supports a “mobile first” approach to API interactions where applications have the freedom to define their own data. It encourages a less “chatty” style of integration as consumers can tailor payloads to meet their individual use cases with a single API call.
Is this solving the right problem?
It’s worth bearing in mind that GraphQL grew out if an ecosystem with a unique set of requirements – i.e. Facebook wanted an API to support a rewrite of their mobile applications. This has worked well for an organisation that has shipped more than a thousand different application versions since 2012. This doesn’t mean that GraphQL is going to be appropriate for any application development.
The flexibility offered by GraphQL can be seductive, but it shouldn’t be used as a hammer for every nail. The ability to address unknown requirements is not always the problem that API design needs to solve.
In most cases API design is informed by a deep understanding of the domain and an appreciation of the longer-term scope of applications. You are likely to have some idea of the kind of abstractions that will make life easier for consumers, which can just as easily be delivered by another style of API.
A good API should be an abstraction that you arrive at after careful consideration of the problem domain. This usually involves understanding how planned features can map onto underlying data stores, processes and legacy systems. A query language might be the answer to your problems, but it will not always be a good fit for a problem domain that is inherently task or workflow based.
How does this compare with REST?
An API based on GraphQL is only as good as its schema design. Perhaps the flexibility provided by GraphQL is only a genuine improvement over badly-designed REST.
As with REST, GraphQL is only an architectural style and it can give rise to very different schema designs. For example, you are expected to implement generic features such as pagination, ordering and authorization yourself. The GraphQL documentation provides guidance on the recommended approach for these features, but they are not baked in to the query language.
GraphQL is protocol agnostic and can be used with anything that allows you to send and receive a string. This means that when it is used over HTTP it does not use protocol features that developers may be familiar with such as methods and status codes. For example, you manually check every response for an error collection as a 200 OK status will be returned even if the request failed.
A GraphQL schema can also become very complex. It implements its own type system, which gets involved once you get into interfaces, union types and input types. This arguably gives rise to greater learning curve than the more familiar resource-based mechanics of REST. Client applications tend to interface with GraphQL APIs via a client library to handle the complexity of query formatting.
The documentation gap
An API is supposed to provide a consumer-friendly abstraction. Consumers may like the idea of being able to shape their responses, but they still need to understand an inventory of queries and mutations that must be orchestrated into processes.
GraphQL’s introspection feature may allow developers to understand the content of a schema, but this does not provide any context. You still need to explain to developers how to execute complex processes through the API’s schema. There is no facility within GraphQL’s Introspection to provide this kind of contextual description.
The same could be said for REST APIs, of course. A full hypermedia API is supposed to be traversable without the need for documentation. Self-explanatory links should be returned with every resource to describe the actions that the server supports. The problem is that it is difficult to communicate meaningful context with these links or describe the wider process. Attempts to fill this documentation gap with tooling such as Swagger has had mixed results.
GraphQL does have some advantages in providing backwards compatibility. The extra flexibility of the query format does mean that GraphQL APIs are less likely to be undermined by small incremental changes to meet emerging requirements. The APIs may also encourage consumers to be more tolerant of change by making it easier to retrieve the data they need, though this kind of behaviour cannot be enforced by the server.
That said, claims that GraphQL makes it easier to manage backwards compatibility can be a little over-played. It may be tolerant of change, but it’s certainly not version free. Facebook claim that they have been able to support three years’ worth of released applications with the same API version. This should be possible with a carefully curated REST API as new fields can be added and obsolete fields deprecated in both styles of API.
Writing cheques you can’t afford to cash
GraphQL is often compared to OData, a comprehensive query language built on top of REST. OData implementations tend to map data tables directly onto REST resources that can have queries applied to them via URL arguments. This forces you into an architecture that couples the OData query engine to the underlying data store.
This creates scalability challenges, as you need to ensure that the data architecture can support the OData query interface. GraphQL can suffer from similar problems, especially if you are using it to pass unoptimized queries directly at a data store.
There are some architectural challenges to implementing GraphQL at scale. It does provide an explicit abstraction between the query engine and underlying data store, but its up to you to decide what query operations you want to support and mitigate any potential performance issues. Mapping GraphQL’s graph-based schema onto a highly normalised relational database isn’t always that straightforward.
The schema inevitably forms a very broad contract between the service and consumer. This gives the server an obligation to support a potentially unpredictable pattern of demand. It can be difficult to plan for capacity and optimization techniques such as request caching may not be straightforward to implement.
You will also need to make choices around where query processing happens. Do you translate a query and pass it to the underlying data store for execution? This can help to minimise the amount of data transferred between API applications and their data stores, but it comes at the cost of greater complexity.
You still need to design an API…
GraphQL may provide some welcome flexibility, but it does not mean that you can expose all your data and let your consumers decide how best to use it. You are still designing an API.
This requires an investment in good design to expose the kind of abstraction that consumers need. Designing an infrastructure to support the resulting API traffic can also be non-trivial, particularly if your data and processes do not immediately fit the graph-based model it prefers.
GraphQL can seem particularly attractive when APIs and applications are developed by separate teams. Cross-team dependencies can be painful to manage, particularly for the kind of frequent, minor changes required in mobile development. GraphQL lessens the impact of these changes by requiring fewer changes to APIs to accommodate them.