Things you should know about GraphQl in 2024
Over the years, GraphQL has become quite a big deal. Many developers were excited about this technology, and I was one of them.
Check out our 2021 series:
GraphQL Overview Part 1: What is GraphQL?
GraphQL Overview Part 2: Libraries
GraphQL Overview Part 3: The Infrastructure and Summary
After a few years of using this technology, I would like to share my thoughts and knowledge about GraphQL.
Let's start with a simple definition, what is GraphQL?
GraphQL is a query language that lets you decide what kind of data you will receive in response to the request. GraphQL also provides runtime for fulfilling those queries with your existing data. About this data, you can think in the fashion of nodes in graphs. Objects may have a relationship with other objects, but you may be interested only in a small portion of those nodes.
Simple? Excellent, let's go deeper.
Freedom
GraphQL gives clients of the service freedom when it comes to choosing what they will receive from the server. You control what information you will retrieve and which fields are unnecessary in your case for each request, either its query, mutation, or subscription. Those are the only 3 types of requests. Query operations are designed for retrieving data, Mutation operations for modifying it, and Subscription operations provide real-time updates when new data becomes available.
As a client, you can request all the data you need in a single query. If you are interested in some parts of the data, you can just request them. In different parts of your system/UI, you can choose different data.
Thanks to that, the provider of the API does not need to think about every edge case of data usage. That’s very sufficient in situations with multiple clients using your public API. Because clients can use your data in various ways.
That idea affects our development process in a significant way. Teams, or parts of the teams like the backend and the frontend can be more asynchronous in communication. That’s because we have a contract between provider and consumer - a schema that we will describe later.
The nature of GraphQL makes data objects connected. So, when we add a field for an object, all query responses that return this object type can now have one more field. There is no need to ask the backend team for a single field in every endpoint.
When you add a new field in the schema, you don’t need to inform all the clients of this schema because they will not use this data. Although, maybe you should if you work together as a Team.
Is it mean, that the backend developer will create a big schema and leave everything to clients? Of course not. There are many situations when some requests have to be optimized for fetching more data or filtering them.
That freedom of choice can also make your payload a lot smaller. For example, in Migrating to GraphQL: A Practical Assessment - where authors migrated GitHub API from REST to GraphQL they decreased payload from 93.5 fields (median values) to only 5.5 after migration to GraphQL. This affects the size of responses from 9.8 MB (median values) to only 86 KB after moving to GraphQL. That's great news for mobile applications using the same API as web applications.
That was the reason why GraphQL was created in the first place. Here is the oldest article from Facebook about it.
Schema
I’ve mentioned a schema. It is somewhat a place where developers can discuss how queries/subscriptions and mutations will look like. For me, that’s one of the biggest features of GraphQL. You can't avoid its creation. And even if you could, you should not, because it gives you control during the development process and creates a contract between the API provider and its clients. It could be a front-end teammate, a different team from another continent, or even an external company.
GraphQL schema is strongly typed thanks to that, tools can support us by checking if our request is correct with the schema. And also, the GraphQL server validates request correctness. Providers use schema to create correct entry points to service.
Schema structure is hierarchical, meaning that some fields can be nested (custom types). Thanks to that you can avoid many requests to the service, especially if you fetch data around a single node/aggregate. But, not every time, you will be able to go over the graph and gather all the information. Some nodes are not connected in the GraphQL schema. In that case, we can solve that by executing more than one operation in a single request.
Schema can be provided to its client in a few ways. You could share a file with the client or the client could use an introspection query to receive a schema. It’s a good practice to disable introspection queries on your production service.
Here is more information on why and how to do that.
Schema often takes a burden from us, when we need to think What endpoint should I query for this use case?
It’s not my problem now, GraphQL will provide me with a schema and if there is information in the schema I can fetch that.
Code Generation
Because GraphQL provides us with a schema to work with, some tools can generate structures to use in code, both for the Backend and the Frontend. This simple feature lets you skip the boring creation/edition of classes with every change in the schema. As an example in Java, you may use graphql-java-codegen
. That could be particularly useful in a larger project where we are not aware of all changes in the schema. Then we just fetch a new version of the schema, rebuild the code and everything is up-to-date. Also, that prevents us from making simple errors like typos in fields. Thanks to code generation you will most probably save some time.
Evolution
GraphQL is defined as version free
, which means that if you will do it right, you don’t need to version your schema.
We still support three years of released Facebook applications on the same version of our GraphQL API.
GraphQL a data query language
How is it even possible?
The addition of the new field will not break any contract, the client does not need to even know about those new fields or queries right?
Ok, but what about removing some features? Here @Deprecated directive comes to the rescue.
Let's answer what the directive is.
In simplest words, that is a piece of additional information that you can provide in the schema. You can also implement logic around directives, so it's quite powerful.
Now, let's go back to @Deprecated.
usage of @Deprecated directive
This is a way to indicate that this part of the schema is no longer recommended for use by clients. But, this field will still be in our schema even if we annotate it with @Deprecated and after some years we may end up with a bunch of them. That’s why we should observe how our schema is used. When you are certain that nobody uses this field you can remove it.
Observability is another topic for a different blog post, but you can look into Apollo Studio, which allows you to check statistics about your schema, requests, failure rate, and many others, that can help you decide if some field is ready to remove.
The evolution of the GraphQL schema will be a gradual process, not a big-bang change.
Of course, if you are working in a small team or have good communication with clients, you can remove a field if both sides are ready for it without long-lived @Deprecated fields. As always it all depends.
Inefficient Queries
GraphQL gives us a lot of freedom and possibilities, but as always, nothing comes without a price. In our schema, most of the objects are connected, which can cause a situation, where some developers may want to fetch all of the data. Also, some of the data may be nested and we could also fetch them.
But, where is the problem? We have those data in a schema, why cannot I just pull them?
From the GraphQL point of view, every field is a function. In many cases, this resolver method is quite simple, just a get method from the object. Often the backend tools are hiding that from us because we just work with objects. Some fields are different, they need us to calculate something or make an additional request to a different service just because that kind of data resides in a different place. It's hard to say which field is a costly one.
For example:
Example of the User node
Can we in a simple way distinguish which of those fields require additional requests? It could be accountNumber right? But, maybe name
and lastName
require some obfuscation for security reasons. Also, maybe there will be no additional requests or calculations. We will never be sure what happens after our call. This case is very simple because we have only four fields. What would happen if we had 20 fields with custom nested types? Then, it would be a lot harder.
What are the consequences of that?
N+1 problem
Example of the potential N+1 Problem
We could add to our User a new field that contains all credit cards of some Users. Whenever we fetch a User we could fetch user cards. In this case, the server would want to fetch every element of this collection individually. That could cause an N+1 problem.
Those kinds of issues can be solved by using data loaders. That’s an additional fetching feature that helps your server group nodes and asks for them one time.
Dependency hell
Another problem that could be caused, is that fetching whole objects may do a lot of requests to many services just to fetch a single GraphQL object. But, is it a client's fault that wants a field like an account number? No, of course not. So, how can we address this problem?
I’ve noticed that developers tend to use godly types in Schemas. For example, our User type during the development process could become a monster like that:
Example of the huge node
When we would like to fetch a whole User object, that would hurt, right? Many of those fields are quite complex and they may need to make an additional query to a service. Sometimes to get some of those fields you may need to send a request to an external source of truth(like government servers).
Most probably, no view needs all of that information. So what can we change? We could change the data structure beneath (database or services) or schema above.
If possible, avoid modifying the database schema. Tight coupling between the API and the database is something that you want to avoid. One option is to move data from one service to another. While this could work it’s worth remembering that services have more responsibilities than just gathering data. They are responsible for scalability, business logic, bounded contexts, etc.
Another approach is to create a read model for this particular type. This can work well, but you might encounter issues with eventual consistency on reads.
The last resort would be modifying the GraphQL schema.
It's not wise to base your schema on UI. We can try to use concepts from domain-driven design, particularly ubiquitous language. From the definitions, this is a set of terms that business and development teams share. Both of them should understand those concepts in the same way.
So let's try to split our User into some different concepts.
(If you are working with the banking domain you most probably think that my naming is silly, sorry for that :) )
This split is based on some different contexts that could create separate apps or views for different kinds of users. Most probably people who will give you a mortgage are not the same as people who contact you about credit cards.
Also, every type of information may be fetched from different places. For example, SecurityInformation
contains info, if clients have been hacked. That kind of data does not belong in the debt
domain, right?
Right now, many of those types are not connected. If the split is done right, that is good. Now every query will just fetch information from one place(service/read-model) as fast as possible.
Is it possible to achieve that in production? No, most probably you will have a connection between types like in MortgageInfromation
. However your goal should be to decrease the amount of them.
Is the source of the data the best and only designator for splitting objects in the schema? It’s good, but we should also remember about the read rights.
What I can see?
Whenever we process a request from a user, we need to check who this user is and if this user has the correct rights to fetch some data. Let's now focus on our Account
type.
Let’s imagine that the field loans
is possible to be fetched only by a few types of users. So what should we do, when a user who fetches a whole object does not have access to this field? Should we throw an exception? That sounds correct because something went wrong on server side. There is also a second approach. We could return a partial object. Then all fields would be available but, what value will we have on the field loans
? We could return null, but “!” in the schema indicates that there will be no null field. Therefore, maybe we should return an empty list?
That's a small but important decision because we could introduce an inconsistency in our application. Now, some clients can fetch an empty list of loans, even if they exist. Your user can make a business decision based on this data. That’s not good, right?
Maybe when I have access to the Account type should I be able to fetch all fields? Now we would have permissions on the root type of the query. That is an interesting approach that simplifies the authorization issue.
Of course, nothing is for free, when we do that we may feel restricted in API design. When the domain changes, we still could end up in this situation. You can help your API consumers with custom directive. We talked about @Deprecated directive, but you can create your own, custom one. For example, you can create a directive with the name @hasRole where you provide the needed role, and then in runtime check if the user has it.
Custom directive example
Security
Is GraphQL safe? As always the answer should be “It depends”. But, I’m sure you are exhausted from hearing these words. So let’s re-phrase the question - “Will my app be safe if I just addGraphQL to the stack?”. Then the answer is no. As in most cases, if we give our users a lot of freedom, they may use this freedom to harm you. One of the common mistakes is having enabled introspection query on production, then we provide users of our product with the possibility to fetch queries and data that they should never see. In that situation custom directives like @hasRole give a lot of knowledge for potential attackers.
This topic is important to go deep into, and deserves another article about it. Do not ignore this, because it may cost your production a lot of harm.
Although if you would like to learn about GraphQL vulnerabilities in a safe environment I highly recommend you project Damn Vulnerable GraphQL Application. It’s a hack me
application with a lot of vulnerabilities to take advantage of. Have fun :)
GraphQL implementations
It’s worth noticing that not every implementation of GraphQL server may have all features.
Why is that? GraphQL specification was created in 2015, and the last official update was in 2021. Our industry evolves all the time, so some decisions had to be made.
For example, what HTTP response status should we return? You may think it will be 200, and yes, 200 OK is "common practice", but GraphQL spec nowhere specifies what code the server should return. As a consequence of that, some tools specify different codes for errors.
Graphlq-helix or Netlix-dgs has a different approach to that.
That’s a problem for some developers, and for that reason, they created RFC (draft for now), that describes how GraphQL should be served and consumed over HTTP.
The differences are not only about status codes. Some implementations have different approaches to security or even introduce additional types. It’s worth to be aware of that.
Summary
GraphQL is an amazing technology worth looking into. I recommend you play with it, or just look at the Apollo free tutorials. It may give you a fresh look at API, and the development process.
Remember that even if something looks incredible from the outside, if you want to use this in production, take a deeper look at potential cost. Good Luck.
Reviewed by Darek Broda, Mieszko Sabo