by Mugur Stefanescu
There is no doubt that the interest in Facebook’s GraphQL is high and its usage gains acceptance and advocacy. GraphQL is touted as an API query language and as a “better” alternative to REST, yet it is usually described in general qualitative terms, using a somewhat unusual jargon with few references to existing concepts. This article aims to look beyond GraphQL’s common description and to put it in perspective of familiar concepts in computer science. In order to present the perspective, it will omit many of the non-fundamental features, focusing instead on the important ones. Hopefully, the article will help clarify what GraphQL is and what it is not.
Loosely speaking, a query language is a specialized computer language that retrieves (and sometimes, modifies) data from a data source, whether a database or some information system. A crucial feature of a query language is the ability to provide a high degree of client-driven variability in the retrieval and manipulation of returned data, usually through matching, structuring, filtering, ordering, aggregation, etc. In practical terms, a query language allows different clients to return vastly different data in content and structure based on different queries on the same data source.
Looking beyond the syntax, GraphQL is a sophisticated and heavily disguised variation of Remote Procedure Call (RPC) concept. GraphQL allows a service to execute operations that are queries (reads), mutations (writes + reads) and subscriptions (asynchronous notifications). Its main strength is its way to customize operations, avoiding over-fetching, i.e. returning more data that needed, and limiting under-fetching, i.e. needing multiple requests for returning dependent data. GraphQL limits over-fetching by requiring the client to specify the subset of fields returned for each call to execute an operation. Besides obvious economy of bandwidth and processing needs for unmarshalling the data, this feature has the potential for substantial server-side optimizations. A GraphQL query can also limit data under-fetching by allowing a query to express simple navigations between explicitly related entities. These two features provide a good balance between variability and simplicity and make GraphQL a useful and practical API interface for a domain whose logic is well established and whose logical schema describes explicitly relationships between entities. This balance contrasts with more general graph query languages, like Cypher or Gremlin, or relational algebra based query languages like SQL and XQuery. These languages provide much more client-driven variability to the detriment of simplicity; they have complex capabilities to deal with relations that are not explicit in the schema and can perform data matching, grouping, complex aggregation, query composition, etc.
The Basics
A service accessible via GraphQL defines both a schema to describe its business domain and a set of top-level operations that can be performed on the domain data. It does so using a unified syntax that allows type fields and operations to look like function signatures. In particular, the syntax allows a data field to be indistinguishable from a function with no arguments. A properly designed schema allows GraphQL to see the domain data as a directed, connected, edge-labeled graph, in which the nodes represent instances of business entities and edges represent named, explicit, directed relationships. In the schema representation, business entities are abstracted as instances of object types, which are named sets of typed fields, a familiar concept from many languages. Furthermore, entity properties are represented as scalar types (string, integer, Boolean, etc.) fields. Relationships to other entities are represented as fields of object types (one-to-one relationship) or arrays of object types (one-to-many relationship). For instance, the following type in GraphQL schema language represents an entity with two scalar fields (name and age), a field representing a one-to-one relationship (“favAuthor”) and a field representing a one-to-many relationship (“friends”):
type Person { name: String, age: Int, favAuthor: Person, friends: [Person] }
which is roughly equivalent to the following entity relationship diagram:
For any instance representing a type, returning a field of a non-scalar type is equivalent to navigating the data graph from the node that it represents to one or more immediate neighbor nodes across the relationship with the field’s name. For instance, the following query returning a particular person’s name, favorite author and for each of his friends, the name and the name of their favorite author:
query {selfAndfriendsFavAuthors(id: 100){ name, favAuthor {name}, friends {name, favAuthor {name}}}}
is a navigation of the data graph shown below. The navigation starts from the object representing the person with the id=100, passed as an argument, and visits the object representing the person’s favorite author, the objects representing the person’s friends and from there visits the nodes that represent their favorite author, collecting the name scalar fields on the way:
This feature is both elegant and very useful, as it represents a common use case: identify a node by some criteria, like an object id, and selectively navigate from it to its immediate neighbor nodes and so forth so on. To accomplish node navigation, an implementing GraphQL service usually needs to orchestrate multiple retrievals: one for accessing the starting node and one or more for accessing the neighbors.
The core defining features are complemented by a host of other features that, while very helpful for its usability, do not change the fundamental capabilities of the language. These features include aliases, variables, fragments, input values and list type inference.
Venues of Extensibility: The Finer Points
Besides a basic type extension capability, familiar from many other languages, GraphQL provides two subtler, but very powerful ways to extend its capabilities: the directives and the implicit optionality of fields in a type.
The directives are processing instructions that optionally can take arguments and can provide custom behavior. Currently, GraphQL has two built-in directives, @skip and @include for basic conditional processing, but it also allows an implementation to define custom directives that could provide much more complex behavior.
Unlike directives, which are clearly defined in the language specification, the fact that type fields are implicitly optional is an unspecified and much subtler venue for extending the behavior of a GraphQL system. Since type fields are implicitly optional, a GraphQL system could inject implicit fields to all types, similarly to the pseudo-columns in relational database management systems like Oracle and Informix. Given that pseudo-columns behave like functions and type fields in GraphQL can have arguments, as they really are field accessor functions, it results that a GraphQL system could provide significant extensibility via library functions. The obvious candidates for this kind of extensions are aggregate functions typical in relational database systems: sum, min, max, average, count, etc. For instance, applied to the previous example, an implementation might choose either to inject implicitly or extend from a system base interface, a field aggregates of a type possibly defined as following:
type Aggregates { sum(field: String): Number, average(field: String): Number, count(field: String): Number, … }
A more elegant solution could capture the name of the field in a path expression, with the field part generated dynamically, so aggregates.friends.count would provide the count of friends for a given person.
In a Nutshell
As a Query Language
As a query language, GraphQL represents an minimalist approach, prioritizing simplicity over power. Its great strengths are simplicity, schema enforcement and efficiency for very common use cases. Its weakness is limited “off-the-shelf” client-driven variability: limiting the fields returned and navigation to explicitly related nodes. Like RPC and unlike traditional query languages, to increase client-driven variability, a GraphQL system has to add or change an operation described in its schema. However, the minimalist approach in off-the shelf capabilities is supplanted by the potential for providing complex functionality in a particular implementation of GraphQL through custom directives and custom usage of implicit fields.
As an API Protocol
As an API protocol, GraphQL should be compared both to “traditional” RPC and to REST. Arguably, compared to traditional RPC, GraphQL, itself a variant of RPC in disguise is an improvement. Its contributions consist of making idempotent operations explicit, thus making retries and caching easier, and having simple, built-in mechanisms to avoid over-fetching and limiting under-fetching. However, GraphQL improvements over traditional RPC are less relevant, when the business domain is not a highly connected graph and when the client needs all the fields of an object for each call.
Comparison to REST is a bit more complicated, as the REST terminology and practice have unfortunate variations. To simplify, one should make the distinction between RESTful APIs, based on the architectural pattern REST as originally described, and RESTish or “pragmatic” REST APIs, which are based on an incomplete form of REST. Both are centered on common HTTP concepts, like addressable resources, usually representing business entities, and the ability to distinguish idempotent operations, thus simplifying retries and allowing caching with HTTP typical mechanisms. “Orthodox” REST also mandates the use of hypermedia to drive a client/server interaction (HATEOAS), similar to the state transition in a state machine. This feature allows the client-server interaction to be less coupled, which provides an additional level of robustness against service evolution. Unfortunately, REST is not formally specified and industry has a wide range of implementations with their own idiosyncrasies.
Compared to REST, GraphQL is well defined as it has a formal specification. Its fundamental advantage is that it has a built-in, well-specified mechanism for avoiding over-fetching and limiting under-fetching. However, GraphQL has a number of disadvantages, which could be relevant to a client. Unlike any form of REST, a GraphQL service will require a specialized, GraphQL-aware mechanism for caching. Furthermore, GraphQL does not naturally provide something similar to HATEOAS to drive the client/server interaction; the interaction is entirely driven by the client that needs to be cognizant of the state and next possible steps in the interaction.
In the spirit of demystification, one should note that many comparisons of API paradigms are somewhat biased. Two of the most common biases are comparison to RESTish, rather than “orthodox” REST, and the claim that GraphQL is somewhat uniquely not affected by the need for versions. In reality, both RESTful and GraphQL APIs can and should use some form of graceful deprecation and both have the same difficulties if the semantics of the requests and responses changes. Furthermore, both RPC and REST implementations could be designed to add GraphQL’s capabilities of field selection and navigation to immediate nodes. Yet, the main drawback is that, in the absence of a specification, each of these designs would be one-offs. Conversely, a GraphQL system could be designed to be consistent with the REST architectural style and provide equivalent HATEOUS behavior, yet it would not be elegant and it would depend on conventions and design decisions that are not captured in its specification.
Conclusion
GraphQL is a sophisticated variant of the RPC paradigm that balances simplicity, conciseness and power, with a strong bias towards simplicity. Its crucial value is that is has built-in mechanisms to combat over-fetching and limit under-fetching. These features provide justification for the claim of being a query language and the ability to work with graph data. While being significantly more limited in power compared to more traditional query languages, GraphQL does provide powerful ways to extend its off-the-shelf behavior to close the gap. Lastly, GraphQL is backed by a language specification thus ensuring a high degree of compatibility between implementations.