GraphQL as a powerful abstraction layer


6 min read

By now I'm sure you've heard of GraphQL. There is a good chance you are even using it in something, or at least you have in the past. There are lots of tutorials, conference talks, blog posts, and even books about it. And yet, in everything I have come across, one of the biggest benefits that GraphQL provides is often overlooked. So I thought I would talk about it today.

A brief recap of the obvious benefits of GraphQL

Let's start with a quick recap what GraphQL gives you that you commonly see around the internet. If you would like to know more, please visit the official GraphQL page here

  • Defined schema - This is beneficial in that you have clear line of communication between the server and the client. Types if data for inputs and outputs are defined through a set of base primitives that can be built up to represent complex objects and relationships. The schema can also be stepped through interactively with tools like GraphiQL, which also allow markdown style notation on fields and queries, so schemas become self documenting.

  • Clients can shape data as needed to speed development - Since the GraphQL paradigm allows clients to request data in the shape that they need, only what is needed is sent in the response. This leads to lighter payloads over the network on each request, which in turn provides quicker API responses.

  • Solves the n+1 problem - Related data can be grouped in a single request and response, meaning the "waterfall" problem of requesting connected data through a series of requests is no longer an issue.

These are all huge benefits to be sure, but there is one thing that could be added to this list that might help people to better understand and adopt this technology:

GraphQL is a powerful boundary between layers of abstraction

This is the way I see GraphQL in my head: the GraphQL schema is a wall with defined ports. On one side of the wall, you have your server or other API functionality. On the other, you have your client. This wall happens to align perfectly with the network between server and client. (Not getting into the low level details of running a server - receiving HTTP request and sending responses, CORS, etc.) So the schema you design for GraphQL represents the border between 2 distinct contexts at 2 distinct layers of abstraction. It also provides the interface, the protocol, between these layers that will allow them to communicate.

Now, to be clear, you could possibly achieve something similar with other protocols, and there have been quite a few attempts to so this in various ways. gRPC from google and their protobuf files is one such attempt, as is OpenAPI. Each of these has their merits - and weaknesses - that must be considered before use. In my opinion, neither of these really hit the mark though. You still have the problem of too much or too little data on each request, leading to multiple requests. Furthermore, especially in the case of gRPC, the more detailed you make a back end service and it's related endpoint, the more a client knows about the server implementation, which as we all know is best avoided in accordance with best practices.

GraphQL give us a more elegant solution and a clear dividing line between front end and back end.

GraphQL enforces separation of concerns at a high level

With GraphQL as the wall between the server and client side, neither side needs to be concerned with what the other is doing. Client concerns stop at the GraphQL schema. Server concerns likewise stop at the same schema. So you have effectively split the two by both layer of abstraction and implementation details.

Now the front end is only concerned about the UI portion and anything related to displaying information from the server. What the server does to generate data or save it in the event of a mutation isn't relevant - it can't be relevant, because the client's ability to interact with the data stops at the GraphQL schema.

Similarly, the server can't know what the client is doing with the data that the server send out, or how the client is collecting data for mutations that are submitted. Again, it's all on the other side of the wall and is no longer relevant. As long as the server is fulfilling it's duty to respond to the schema, it's work is done regardless of what the client is up to.

This gives the people working on each side of the wall Autonomy. Once something is defined in the schema, each side can begin work without thinking about the other, and can work on things in whatever order they see fit at whatever pace they see fit. The only requirement still in place is that the GraphQL implementation on the back end be functional by the time the front end deploys with queries to the schema. That's it.

Further, adding to the schema is as simple as adding a type to shape the data, possibly some queries and mutations for said type, and connections to other types if applicable. The schema is still backward-compatible with existing client code as long as nothing is deleted, so running production systems can adopt an asynchronous approach between front end and back end teams. Change management becomes much easier as a result.

GraphQL discourages coupling between server and client

The separation of concerns and related autonomy mentioned above has another added benefit: Once the server and client have a strong separation of concerns they are inherently less coupled to each other. In fact, the client could be completely rewritten in a different framework and the server wouldn't need to change a thing. Likewise, the server could be rewritten in another programming language or divided into microservices, and the client would never know. This is loose coupling in action. Since the schema becomes the known standard that all sides are working toward, nobody needs to know what's on the other side of the wall for the system to function.

GraphQL increases testability

Yet another benefit of high abstraction is that code is easier to test. On the client side, since the schema of the data from the server is known, it is very easy to mock inbound data to run tests independent of the server. Failure states can also be mocked because it becomes clear what those states look like. When it comes time to go to production, as long as the GraphQL API is running, the client just needs to point to it and the rest should work as expected.

On the server side, the schema defines all known entry points to the data and all known methods of combining data in a request, so again it's easier to mock test inputs to get outputs and a running client is not required. (I should say relatively easier - testing these kinds of systems is not easy any way you slice it) Error states are defined, possible null values are defined, so the whole thing becomes easier to understand and reason about. As a result, it's easier to build out tests and verify the behavior of the server, no client required.

I hope you find my take on Graphql as an abstraction useful. Thinking about it this way has helped me to better understand and adopt the technology and to build more flexible codebases as a result. If you have anything to add, please drop a comment below.