The article summarises lessons learned while working on a project that applies GraphQL and REpresentational State Transfer (REST) as paradigms for providing web services.
The purpose is to illustrate and motivate the context and the factors that drove their adoption.
Why did we choose GraphQL?
GraphQL provides a front controller on top of heterogeneous data sources, whether the provider is an API, a database, or an arbitrary application that provides data through an interoperable protocol.
A GraphQL-centric approach offers many simplifications regarding data access/manipulation over the well-known REST paradigm.
Client-driven vs Server-driven
A RESTful system models a given domain with a variety of resource-centric endpoints. While that provides a clear separation of concerns regarding system cohesion (the infrastructure allows one to identify how to deal with a given resource quickly), things become tricky when we start working under the surface: for example, what about the exchange between client and server? What about message payloads?
GET requests are used for fetching resource representations. Suppose we want to visit
The intent is clear: we want to get back a representation of that resource. The expected payload could be something like this:
{
"firstName": "James",
"lastName": "Smith",
... (other fields)
"pets": [
"https://myapi.org/pets/992",
"https://myapi.org/pets/192",
"https://myapi.org/pets/61"
]
}
The point we would like to highlight is: unless the API doesn’t implement a client capability for driving the content of the represented resource, it’s easy to fall into the following unwanted scenarios:
Over-fetching
We need just the person’s name, while the response contains other information we are not interested in. Besides increasing the exchange payload, the server may have spent more resources than the actual needs.
Under-fetching
We need the person’s name and the name of all pets he/she owns.
The person’s name is there. What about pets? We need three additional requests (one for each pet), with a high chance of falling into three over-fetching scenarios (remember, we need only the pet name).
GraphQL addresses both issues by explicitly allowing the requestor to model the response shape. The request would be
{
person(id:"273") {
firstName
lastName
pets {
name
}
}
}
That clearly states what the client is looking for. Here’s the response:
{
"data": {
"person": {
"name": "James Smith",
pets: [
{ "name": "Bobby" },
{ "name": "Fuffy" },
{ "name": "Lilli" }
]
}
}
}
GraphQL interaction is client-driven: the request contains details about the operation and the wanted response shape.
Type system
Let’s get back to our example: https://myapi.org/people/273.
Assuming we are on the client-side, can we make any assumption about the response we will get? No, we said it could be
{
"firstName": "James",
"lastName": "Smith",
...
"pets": [
"https://myapi.org/pets/992",
"https://myapi.org/pets/192",
"https://myapi.org/pets/61"
]
}
but it could also be something like this:
{
"firstName": "James",
"lastName": "Smith",
...
"pets": [
{
"uri":"https://myapi.org/pets/992",
"name": "Bobby",
...
},
...
]
}
or even:
{
"name": "James Smith",
"pets": "https://myapi.org/people/273/pets"
}
All of them are valid responses; apart from the formal protocol (JSON), the actual content depends entirely on the server.
Is it important? Yes, because the data shape drives the interaction between the client and server: remember, we are looking for the name of the person and the name of their pets:
- in the first example response, we need four requests
- in the second example, just one request and we have all data we need
- in the third example, we need five requests
How does GraphQL deal with that? It provides a feature called introspection, which is one of the building blocks of the protocol: a client can issue a specific metadata query to fetch the schema that drives the client-server interaction.
The schema introspection lets clients know in advance how a person or a pet is structured, its properties, operations, parameters, and return types.
Operations
REST defines a resource interaction model based on HTTP methods. Although the Hypertext Transfer Protocol RFC is very detailed, we cannot deny that it wasn’t created with REST in mind.
That often generates confusion when we have to model interactions regarding mapping to HTTP methods; if the questions below sound familiar, you know what I mean.
- What is the difference between POST and PUT?
- What if my interaction is an UPSERT (e.g., create the entity if it doesn’t exist, update if it exists)? POST or PUT?
- Domain models organized in a hierarchy, a mix of aggregations and compositions: what is the correct approach for creating endpoints?
- Same endpoint, same resource, different parameters that drive the behavior?
Unfortunately, it’s not easy to find the correct answers; sometimes, it’s not a yes/no scenario, and you could even find reasonable solutions on opposite sides.
GraphQL provides a semantic for modeling operations: it introduces three top-level types that encapsulate the concept of interaction:
- Query: for synch data fetching
- Mutation: for domain model state changes
- Subscription: for async data fetching
Why Did We Choose REST?
The project where we applied most of the considerations discussed in this post implements a strong connection with Linked Data and Semantic Web.
Resources organization, identification, and representation are crucial topics that are a bit far from the data access beauties described in the GraphQL section.
REST, in our opinion, is a winner on that side: the paradigm is resource-centric, and the underlying architecture style natively organizes the domain around the concept of identifiable, organizable, and representable information resources.
Client-driven vs Server-driven (part 2)
The static type system imposed by the GraphQL paradigm is a great thing, but in some cases, it poses some questions about interoperability.
REST endpoints can be integrated assuming very basic HTTP interaction capabilities: in a minimal/trivial scenario where a client wants to fetch some data, it needs:
- to be able to execute a GET HTTP request (e.g., https://somewhere.org/people/agazzarini)
- to be able to parse a JSON or even XML response
On the other side, a GraphQL connection assumes on the requestor side more sophisticated capabilities in terms of protocol understanding, introspection, and request/response interaction. That is the price to pay for moving on the client side, the responsibility of defining the response and the interaction shape.
URI Dereferencing
The World Wide Web (WWW) is an information space in which resources are identified by global identifiers called Uniform Resource Identifiers (URI). Resources have one or more representations that can be accessed using HTTP.
Dereferencing a URI means retrieving a resource representation using its URI.
Here’s where REST comes to the rescue: endpoints are about resources; they are natively associated with URIs; dereferencing a URI is the first trivial thing a client can do in such a context.
We could even build a GraphQL system that uses URIs as identifiers for the entities. There’s nothing wrong with that; however, that is not strictly enforced by the protocol, so that might leave room for different and ambiguous implementations.
Content Type Negotiation
From the previous paragraph:
Resources have one or more representations that can be accessed using HTTP
When a URI is dereferenced, the client and server can decide about a given resource representation (among those available) using two possible types of content negotiation: server-driven and agent-driven negotiation.
At the end of the interaction, the server returns the resource representation using the negotiated media type and its capabilities.
It should be pretty clear the entire mechanism assumes a system where resources are organized and identified by URIs. As we said, URIs are the building block of a RESTFul infrastructure.
Conversely, GraphQL has a fixed (JSON) response format for returning responses.
Pseudo-Schemaless
In the GraphQL section, we said the introspection capability based on a declarative schema had been one of the key factors that drove us in its adoption.
However, we met scenarios where that constraint was more than an advantage. Here’s a description of one of them.
Share-VDE is a library-driven initiative that brings together the bibliographic catalogs and authority files of a community of libraries in a shared discovery environment based on linked data.
Users can search across a vast catalog through a simple and intuitive interface.
As part of the platform search services, there’s an “explain” feature to provide information about the relationships between one or more terms and a given entity.
A typical read path is something like this:
- Search request: the search API allows to search using one or more terms
- Search response: it consists of a ranked list of zero, one, or multiple matches
- Explain request: for each result, it’s possible to invoke the “explain” API to get back the relationship between the entity and the entered search terms.
- Explain response: it contains the target entity and its relationships with the requested terms.
The underlying domain model is deeply nested; the explanation above needs to have a flexible structure to accommodate such nesting.
For example, a search for Dodgson, Charles Lutwìdge could produce two results:
- Alice In Wonderland
- Lewis Carroll
At first sight, it’s not immediately clear why those two results are there. That’s why we implemented the explanation feature: it can provide helpful information like:
“Alice in Wonderland is a work written by Dodgson, Charles Lutwìdge.”
“Carroll, Lewis is also known as Dodgson, Charles Lutwìdge.”
In the examples above, the “distance” between the target and the related entities is different:
- The work (“Alice in Wonderland”) has a relationship with another entity (a person, the author) that has a variant name corresponding to the search terms.
- The person (Lewis Carroll) provides no relationship with other entities: search terms correspond to a variant form of the author’s name.
The arbitrary nesting level we could have in the explanation response is hard to model using a typed system like GraphQL; on the other side, a less rigid REST style allows implementing that structure as a simple map of maps. The first explanation response is:
/opuses/401/explanation?terms=Dodgson Charles Lutwìdge
{
"meta": {
"aut": {
"label": "author",
"language": "eng",
"type": "Role"
}
},
"aut": [
{
"nameAlternative": "Dodgson Charles Lutwìdge"
}
]
}
And this is the response for Lewis Carroll:
/opuses/203/explanation?terms=Dodgson Charles Lutwìdge
{
"nameAlternative": "Dodgson Charles Lutwìdge"
}
Conclusions
The implicit conjunction between GraphQL and REST in the title of this article is not a coincidence: the internet is full of blog posts that compare the two technologies as if they were opponent alternatives.
In this unpretentious contribution, we listed and explained the reasons that drove us to their adoption. The abstraction level has been set as high as possible to simplify the reading without reporting internal details about the project where we implemented everything we described.
However, if you have some specific questions or are interested in hearing more about what we do, feel free to contact us.
See you next time, and remember, any feedback is warmly welcome!
References
Facebook API Graph
https://developers.facebook.com/docs/graph-api
GraphQL: a data query language
https://engineering.fb.com/2015/09/14/core-data/graphql-a-data-query-language
Graphql.org
https://graphql.org
Hypertext Transfer Protocol — HTTP/1.1
https://datatracker.ietf.org/doc/html/rfc2616
Dereferencing HTTP URIs
https://www.w3.org/2001/tag/doc/httpRange-14/HttpRange-14.html
HTTP/1.1: Content Negotiation
https://www.w3.org/Protocols/rfc2616/rfc2616-sec12.html