GraphQL is dead!
Long live GraphQL!
Birth
GraphQL's origin story is an old one: Developers at Facebook created GraphQL in 2012 as part of the social media giant's shift from web to mobile.
They had a high-performance object-oriented data platform as a foundation. They needed a flexible and expressive web API on top of it to satisfy the needs of nimble mobile developers iterating rapidly on an inherently dynamic, graph-oriented application. They revamped and open sourced GraphQL in 2015 to great fanfare, with the Facebook marketing might behind it. It got a standard, a standards body, and a community.
Along the way, GraphQL's value proposition settled on a few simple promises:
- Ask for what you need, and get exactly that.
- Describe what's possible with a type system.
- Move faster with powerful developer tools.
- Evolve your API without versions.
- Bring your own data and code.
And, it delivered!
GraphQL is a general-purpose query language that gives API consumers the ability to ask, in a single request, for all the data they need. It gave API producers the ability to say in a single response exactly which data they offer and how. It gave frontend developers the ability to iterate independently of backend developers. It gave backend developers the ability to evolve independently of frontend developers. It gave middleware developers the ability to help frontend and backend developers without needing to consult with either. And, it gave architects and data governors the ability to fulfill their dreams of harmonic convergence.
GraphQL had its Technology Trigger and was on its way up.
Life
GraphQL may have crested the Peak of Inflated Expectations since, but it's reached a broad flat plateau where the weather is mild and the terrain is gentle. There have been laudable successes and no overt failures. Needs were met, products were created, wars were fought, and empires were founded.
When plateaus inevitably end, however, pioneers have to plan a difficult descent and journey beyond. The bloom is off the GraphQL rose and not all is a bed of roses. Discontentment rises and disillusionment beckons and the tools and tricks that got us here may not work for what lies ahead.
Death
Rumors of GraphQL's demise are greatly exaggerated. GraphQL is far from extinct, but what environmental conditions threaten its path to flourishing? An emerging consensus groups the various grumblings about GraphQL into three main complaints.
- Security
GraphQL exposes public APIs to exfiltration, tampering, and denial of service from bad actors. - Performance
GraphQL exposes all APIs to bad performance even from friendly actors. - Complexity
GraphQL exposes developers – especially backend developers – to enormous complexity.
In short, if GraphQL APIs are a dream to use, they're a nightmare to create, especially if security and performance needs are to be met. GraphQL's critics have valid points. Addressing those points and leading it out of a dark wood may require rethinking GraphQL from the ground up.
Rebirth
What is GraphQL and how is it implemented? The GraphQL Foundation answers those questions in this way:
“GraphQL is a query language for your API… [and] is created by… providing functions for each field on each type."
It's time to rethink both parts of that answer.
Rethinking what GraphQL is
GraphQL is not necessarily a "query language for your API." Public and partner APIs don't control or even know their clients, can't control or even anticipate usage patterns, and therefore would benefit from a flexible query language. And yet, there are few public and partner GraphQL APIs.
Anecdotal evidence suggests that the security criticisms of GraphQL listed above constitute the main barrier to GraphQL adoption for public APIs. While there exist GraphQL security measures that can blunt attacks, we should remember that the market for public APIs may be small and may be getting smaller as enthusiasm for public and even partner APIs wanes.
That leaves private APIs. Yet private APIs typically own their clients, know their usage patterns, and don't need a flexible query language. So then, what is GraphQL in this setting?
GraphQL is a development tool. An emerging rebuttal to the criticisms of GraphQL listed above is to use persisted queries, allow lists, and RESTified endpoints. That blunts the security criticism against GraphQL APIs largely by removing GraphQL APIs and substituting them with good old-fashioned REST.
GraphQL becomes a way to define API endpoints. It's an alternative to defining API endpoints in code, but code is a development tool. So then, what is GraphQL but a development tool? If GraphQL is a development tool, then what are its benefits (and its drawbacks)?
The benefits of GraphQL over code as a development tool for defining API endpoints accrue to its original strengths: flexibility, efficiency, and discoverability. GraphQL is a lingua franca, a ubiquitous language to mediate conversations among frontend developers, backend developers, data modelers, and product owners, but it's also an implementation language for the outcome of those conversations. It covers 80% of what code is being asked to do when building APIs – data marshaling – better than code can do it.
The drawbacks of GraphQL over code as a development tool for defining API endpoints lie in that remaining 20%: "business logic." API server code is a convenient if far from ideal arena for implementing business logic.
Business logic, unlike data marshaling, really does benefit from the power of code. If you're using code to write your API server, you might as well just use the same code to implement the business logic right in the API server. Never mind that this mixes concerns and leads to tight coupling, it at least has the virtue of being obvious.
But these drawbacks can be overcome by building a GraphQL server that has no intrinsic business logic, but instead offers channels for injecting business logic:
- Input validation
Synchronous RPC into the language of your choice for the simple but common case of validating input data - Remote actions
Synchronous RPC into the language of your choice for the less simple but less common general business-logic case - Event triggers
Asynchronous event processor in the language of your choice for the common case of exhibiting side effects - Authorization
Elegant strategy for the common but thorny problem of access control
Not only do these features equal the "obvious" strategy for the business logic of writing it right into the server code, but they also exceed it by freeing it from the language of the server and by separating it from the concerns of the server.
Rethinking what GraphQL is by thinking of it as a development tool frees us from a major impediment to GraphQL's progress into the future. The other major impediment is removed as we will see by rethinking how GraphQL is implemented.
Listing 1: GraphQL as a development tool: from Hasura GraphQL definition…
query GetOnePost {
post(limit: 1) {
id
content
comments(limit: 1) {
id
content
replies(limit: 1) {
id
content
}
}
}
}
Listing 2: GraphQL as a development tool: …to Hasura REST API!
GET https://renewed-manatee-36.hasura.app/api/rest/getonepost
{
"post": [
{
"id": 1,
"content": "This is my first post!",
"comments": [
{
"id": 1,
"content": "I love your post! Can't wait for more.",
"replies": [
{
"id": 1,
"content": "Thank you! I'm glad you enjoyed it."
}
]
}
]
}
]
}
Rethinking how GraphQL is implemented
GraphQL need not necessarily be implemented by "providing functions for each field on each type." Those functions are typically called resolvers. They are units of composition in the way servers typically execute GraphQL operations. Their granularity and composability fulfill the promise of GraphQL flexibility. Yet they also are to blame for the performance criticisms of GraphQL listed above.
Not all servers, GraphQL or otherwise, are over SQL databases, but many are. And if we use SQL databases as a representative case, the granularity and composability of resolvers also gives rise to the N+1 problem: A query graph whose node number grows exponentially with every level leads to a call graph whose resolver function calls grow exponentially, which leads to a batch of SQL statements that grows exponentially.
Never mind that the retrieved data volume also grows exponentially with every level, which imposes a natural limit on how far this can go, this is still a problem. And yet the leading solution to this problem – the DataLoader pattern – only diminishes the problem without banishing it, sacrifices much of GraphQL's flexibility, and contributes to the complexity (which is the third criticism listed above). Confronted with this Gordian knot, it's worth asking another foundational question.
Can we implement GraphQL in some other way, and if so, how?
GraphQL can be implemented with a compiler. GraphQL is a general-purpose query language that often, if not always, is over data that ultimately comes from database engines with their general-purpose query languages. If those engines happen to process their queries with a network of operators that bear more than a passing resemblance to resolvers, that's their business. All we have to do is compile (or translate or transpile) our query into theirs and let them handle the rest. Some people in the GraphQL community are finally getting around to this idea. Some of us have been here all along.
The benefits of a compiler approach to GraphQL are many. It eliminates the N+1 problem. It paves the way for a powerful strategy for implementing authorization, as discussed above. And, by generating the GraphQL schema from the database schema, it eliminates tedious repetitive boilerplate data modeling that serves high-minded ideals that are impractical at scale.
The drawbacks of a compiler approach to GraphQL are legendary but in that word's first meaning rather than its second: "of, relating to, or characteristic of legend or a legend" – folklore.
The fear is that, by generating the GraphQL schema from the database schema, the database is exposed and the data modeler's hands are tied. That would be a problem if it was true, but it isn't. The GraphQL schema can be curated as much or as little as needed via SQL views, SQL functions, and affordances and configurations within the GraphQL server.
Moreover, the proposed alternative isn't as different as one might think. Deliberately, thoughtfully, lovingly "designing" a GraphQL data model by meticulously writing resolvers is just making the same choices in code as we recommend through configuration, albeit by taking the long way around.
Rethinking how GraphQL is implemented by thinking of it as a compiler opens up new strategies for addressing old problems. It may not be possible in every case. In Facebook's OG use case for creating GraphQL, it had a high-performance object-based data model with many layers of abstraction away from any databases. That's a blessing and a curse. There may be no query language to compile to but, on the other hand, you can string together resolvers to your heart's content and sleep easy at night knowing the high-performance object-based data model's vast reserves of optimized performance will absorb or neutralize any N+1 performance problems thrown at it.
For the rest of us with our puny SQL databases? Let's just generate our GraphQL schema, compile our GraphQL queries, and call it a day.
Listing 1: GraphQL implemented with a compiler: From GraphQL…
query GetOnePost {
post(limit: 1) {
id
content
comments(limit: 1) {
id
content
replies(limit: 1) {
id
content
}
}
}
}
Listing 2: GraphQL implemented with a compiler: …to optimized SQL!
SELECT
coalesce(json_agg("root"), '[]') AS "root"
FROM
(
SELECT
row_to_json(
(
SELECT
"_e"
FROM
(
SELECT
"_root.base"."id" AS "id",
"_root.base"."content" AS "content",
"_root.ar.root.comments"."comments" AS "comments"
-- ...
-- ...
Closing
GraphQL is far from dead but it’s entering a threatening woods prowled by three wolves: security, performance, and complexity. The good news is there's a strategy for getting out of the woods: Rethink what GraphQL is and rethink how GraphQL is implemented.
- GraphQL is a development tool.
Benefit from GraphQL's promises of independence and nimbleness without sacrificing security, by using GraphQL to develop API endpoints. - GraphQL is implemented with a compiler.
Benefit from GraphQL's promises of efficiency and type safety without sacrificing performance and without confronting complexity by implementing GraphQL with a compiler for the underlying data platform's native query interface.
Not every use case can be satisfied just with API endpoints, and not every use case has the luxury of an underlying data platform with a native query interface, but many do. Moreover, they're often the ones most exposed to the perceived problems of security, performance, and complexity.
Life is about trade-offs. Choices have to be made. GraphQL has a choice to live or die, but it won't be made without trade-offs. Cherished ideas about what GraphQL is and how it should be implemented may have to give way to a changing reality. The old ways of thinking about GraphQL may have to die, to give life to new ways of thinking that allow GraphQL to flourish.