GraphQL is a Trap?
This twitter thread blew up on twitter yesterday and I thought I’d go over some of the author’s points in a longer format so we can clear up some misconceptions. Let’s go over them one by one!
[Tweet 1] GraphQL makes your public API equal to a generic database and — worse — a generic graph database. The amount of maintenance work is sky high. Locking the query capabilities down means you’re just doing normal APIs but not locking it down means infinite performance work (view tweet)
Claim 1: GraphQL Makes Your Public API Equal To A Generic Graph Database
According to the author, GraphQL makes our public APIs equal to a generic graph database. If you’ve been hearing about GraphQL a lot in the past years you probably know that this is one of the most common misconceptions about GraphQL. A GraphQL API is as generic or specific as you want it to, just like most API styles out there, although some support generic features by design.
A generic GraphQL API actually goes counter to what most would consider best practice. In fact, the GraphQL Spec has consistently rejected proposals for generic / database-like features (like filtering, ordering, etc).
Avoiding generic/database-like schemas is literally called out on the official GraphQL website as a best practice. It can’t get more clear than this.
Prefer building a GraphQL schema that describes how clients use the data, rather than mirroring the legacy database schema.
Public APIs do tend to be a bit more generic than internal APIs. That’s needed to anticipate a vast variety of use cases and present data in a more “raw” form which sometimes clients need in public API. Not a GraphQL thing though.
Claim 2: The amount of maintenance work is sky high
There’s not much to say about this claim, it’s a nice anecdote. I can throw mine in and say I have not found maintenance any harder with GraphQL than with other API styles. I could actually say it has been way easier to maintain, but I’d be lying to myself because a lot of GraphQL projects are more greenfield and usually start with better maintainability than legacy APIs that have accumulated tech debt. In fact I think maintenance is more about the software you write itself than the specific technology choice in this case. I don’t see a strong case as to why GraphQL would be harder to maintain anywhere in those tweets.
Claim 3: Locking the query capabilities down means you’re just doing normal APIs but not locking it down means infinite performance work
By locking down the author is talking about things like persisted queries here. It implies that leaving a GraphQL “open” (client being able to execute any arbitrary queries) results in infinite performance work. There’s a couple more red flags in the following tweets that might explain what is truly going on here. A well designed and implemented GraphQL API is able to handle arbitrary queries just fine as we’ve seen with all the public APIs out there serving queries in very reasonable times.
And no, locking query capabilities down does not mean you’re just doing “normal” APIs (whatever that means). Things like persisted queries still allow for great client flexibility and server teams don’t need additional work for every new persisted query. The possibilities are exposed by the schema, but the exact use cases are still driven by the client, even when queries are allow-listed this way (usually at build time).
[Tweet 2] It’s difficult to manage object lifecycles in any app because of the graph-y nature of them internally and that gets so much worse when you expose it to the world. Nearly every system is easier to predict and maintain when it can be deliberate about what connections it shares (view tweet)
I would definitely agree, but that does not seem counter to what GraphQL offers. Again, what your GraphQL API exposes is what you chose to expose. There is no need to expose details you deem internal. Things that are connected in GraphQL were deliberately connected by the people designing the schema, that’s the point. Avoiding unpredictable access to objects in GraphQL is very similar to what one would do in a typical resource-based API. Similar thoughts continue in a tweet below.
[Tweet 4] We get enough surprises of bad performance and lifecycle mistakes in non-graph APIs, but asking our users and ourselves to predict those problems when so much can connect to so much from any direction? Not for human minds imo. The problems all become surprises (view tweet)
I agree that is a trade-off in GraphQL. A GraphQL API generally can’t be as optimized as other APIs because of the flexibility it offers. It is part of the deal, and I understand that for some this may be a deal breaker (although the overhead is in general quite small). There are many ways to make this better when building GraphQL APIs, the biggest one probably being proper setup of batched and cached data loading. Security is incredibly similar than with any other APIs, especially if you want to treat object types similar to “resources” or “endpoints”.
[Tweet 5] In a SQL db, typical GraphQL needs nested queries-in-queries-in-queries and unbounded joins and those are well-known to be reliability, performance, code-malleability, and comprehension problems. And these problems are only an expression of the issues of all generic graph APIs (view tweet)
No, GraphQL doesn’t need terrible SQL queries. In fact I’m very curious to how the author got there. That’s not typically the kind of queries a GraphQL execution results in. If anything, naively implemented, GraphQL results in a ton of small queries for every resolver. With dataloader, it looks like a smaller set of queries (a lot of them being
SELECT ... WHERE ... IN queries). Nested queries in queries and unbounded joins is what I’d expect if someone was trying to build resolvers that look ahead or using some sort of GraphQL-to-SQL auto generation. Just not something I’ve seen often in real life.
Oh, and GraphQL is not a generic Graph API 😂
[Last Tweet] There’s a whole other thing to talk about on how it’s often more useful (and even kind) to make systems that are slow in predictable and bounded ways than ones that are unpredictably or unboundedly slow. And “unpredictable” and “unbounded” latency usually show up together (view tweet)
Could not agree more with this last tweet and that’s a fair point. A GraphQL API, especially a public one, can’t be as predictable as server-driven use cases that are known ahead of time and optimized individually. Naively implemented GraphQL APIs can definitely result in highly inefficient data loading. Thankfully, GraphQL-specific observability tooling, data loading techniques and libraries all exist today and have made GraphQL APIs I work on and have used all quite predictive and fast.
If you don’t need GraphQL’s features, it probably won’t be as simple as quickly throwing down a couple RPC endpoints and it does come with a bit more cognitive overhead.
This thread may serve as caution for what not to do when building a GraphQL API. However it did feel like a strawman most of the time, mischaracterizing what GraphQL was built for. Let’s finish on a few tips:
- Don’t design your GraphQL API like it is a graph database. In general, don’t design your APIs according to your database schema.
- Always use async/batch data loading (like dataloader) when you can, don’t create monster SQL queries based on look-aheads or gql-to-sql tools (as a general rule of thumb)
- Avoid generic features if you can’t support them in an efficient way. Add functionality only when needed and when performance is adequate.
- You don’t have to use GraphQL if you don’t feel the problems it was invented to solve.