You may be interested in checking out https://www.youtube.com/watch?v=lhVGdErZuN4, where I talk about the benefits of Relay. This isn't (currently) possible without GraphQL, so it's a pretty compelling case for GraphQL.
But yeah, IMO, GraphQL doesn't justify itself unless you're using a client like Relay, with data masking and fragment colocation.
I would encourage you to write an educated person's critique of GraphQL, because OP's article + https://bessey.dev/blog/2024/05/24/why-im-over-graphql/ etc. suck up all of the oxygen, and no one hears about the genuine issues like that.
(And don't forget lack of generics, no support for interfaces with no fields, lack of closed unions/interfaces, the absolutely silly distinction between unions and interfaces, the fact that the SDL and operation language are two completely different things...)
This is a genuinely accurate critique of GraphQL. We're missing some extremely table-stakes things, like generics, discriminated unions in inputs (and in particular, discriminated unions you can discriminate and use later in the query as one of the variants), closed unions, etc.
Incidentally, v0.5.0 of Isograph just came out! https://isograph.dev/blog/2025/12/14/isograph-0.5.0/ There are lots of DevEx wins in this release, such as the ability to create have an autofix create fields for you. (In Isograph, these would be client fields.)
TLDR the incremental compiler rewrite is finally bearing fruit. Namely, because we no longer have a batch compiler (i.e. we don't bail on the first error), we can
- provide LSP results (hover, goto def, etc) on non-broken parts of your isograph literals, even in the presence of errors
- surface those errors in VSCode, and
- fix those errors with auto-fixes!! (https://www.youtube.com/watch?v=6tNWbVOjpQw&t=314s) Which is to say, select a field that doesn't exist, and let the compiler create the isograph literal declaring it.
Hilariously – react server components largely solves all three of these problems, but developers don't seem to want to understand how or why, or seem to suggest that they don't solve any real problems.
I agree though worth noting that data loader patterns in most pre-RSC react meta frameworks + other frameworks also solve for most of these problems without the complexity of RSC. But RSC has many benefits beyond simplifying and optimizing data fetching that it’s too bad HN commenters hate it (and anything frontend related whatsoever) so much.
If you're interested in an example of really good tooling and DevEx for GraphQL, then may I shamelessly promote this video in which I demonstrate the Isograph VSCode extension: https://www.youtube.com/watch?v=6tNWbVOjpQw
TLDR, you get nice features like: if the field you're selecting doesn't exist, the extension will create the field for you (as a client field.) And your entire app is built of client fields that reference each other and eventually bottom out at server fields.
- you don't have a normalized cache. You may not want one! But if you find yourself annoyed that modifying one entity in one location doesn't automatically cause another view into that same entity to update, it's due to a lack of a normalized cache. And this is a more frequent problem than folks admit. You might go from a detail view to an edit view, modify a few things, then press the back button. You can't reuse cached data without a normalized cache, or without custom logic to keep these items in sync. At scale, it doesn't work.
- Since you don't have a normalized cache, you presumably just refetch instead of updating items in the cache. So you will presumably re-render an entire page in response to changes. Relay will just re-render components whose data has actually changed. In https://quoraengineering.quora.com/Choosing-Quora-s-GraphQL-..., the engineer at Quora points out that as one paginates, one can get hundreds of components on the screen. And each pagination slows the performance of the page, if you're re-rendering the entire page from root.
- Fragments are great. You really want data masking, and not just at the type level. If you stop selecting some data in some component, it may affect the behavior of other components, if they do something like Object.stringify or JSON.keys. But admittedly, type-level data masking + colocation is substantially better than nothing.
- Relay will also generate queries for you. For example, pagination queries, or refetch queries (where you refetch part of a tree with different variables.)
There are lots of great reasons to adopt Relay!
And if you don't like the complexity of Relay, check out isograph (https://isograph.dev), which (hopefully) has better DevEx and a much lower barrier to entry.
And its exchange system is super powerful and flexible. I’ve even seen an offline-first sync engine built as a custom URQL exchange in a react native app. The frontend could be written as if the app always is online but it would handle offline capabilities within the exchange.
- you can make changes to subcomponents without worrying about affecting the behavior of any other subcomponent,
- the query is auto-generated based on the fragment, so you don't have to worry that removing a field (if you stop using it one subcomponent) will accidentally break another subcomponent
In the author's case, they (either) don't care about overfetching (i.e. they avoid removing fields from the GraphQL query), or they're at a scale where only a small number of engineers touch the codebase. (But imagine a shared component, like a user avatar. Imagine it stopped using the email field. How many BFFs would have to be modified to stop fetching the email field? And how much research must go into determining whether any other reachable subcomponent used that email field?)
If moving fast without overhead isn't a priority (or you're not at the scale where it is a problem), or you're not using a tool that leverages GraphQL to enable this speed, then indeed, GraphQL seems like a bad investment! Because it is!
Yes, Apollo not leading people down the correct path has given people a warped perception of what the benefits actually are. Colocation is such a massive improvement that's not really replicated anywhere else - just add your data requirements beside your component and the data "magically" (though not actually magic) gets requested and funnelled to the right place
Apollo essentially only had a single page mentioning this, and it wasn't easy to find, for _years_
Quite. Apollo Client is the problem, IMO, not GraphQL.
Though Relay still needs to work on their documentation: Entrypoints are so excellent and yet still are basically bare API docs that sort of rely on internal Meta shit
100% agree on the unnecessary connection between entrypoints and meta internals. I think this is one of the biggest misses in Relay, and severely limits its usefulness in OSS.
If you're interested in entrypoints without the Meta internals, you may be interested in checking out Isograph (which I work on). See e.g. https://isograph.dev/docs/loadable-fields/, where the data + JS for BlogBody is loaded afterward, i.e. entrypoints. It's as simple as annotating a field (in Isograph, components define fields) with @loadable(lazyLoadArtifact: true).
Neat! I basically just reimplemented some of the missing pieces myself, but honestly for the kind of non-work GraphQL/Relay stuff I do React Router with an entry point-like interface for routes (including children!) to feed in route params to loadQuery and the ref to the route itself got me close enough for my purposes
I’ll have a play though, sounds promising :)
Oh this is interesting, sort of seems like the relay-3d thing in some ways?
Yeah, you can get a lot of features out of the same primitive. The primitive (called loadable fields, but you can think of it as a tool to specify a section of a query as loaded later) allows you to support:
- live queries (call the loadable field in a setInterval)
- pagination (pass different variables and concatenate the result)
- defer
- loading data in response to a click
And if you also combine this with the fact that JS and fragments are statically associated in Relay, you can get:
- entrypoints
- 3D (if you just defer components within a type refinement, e.g. here we load ad items only when we encounter an item with typename AdItem https://github.com/isographlabs/isograph/blob/627be45972fc47.... asAdItem is a field that compiles to ... on AdItem in the actual query text)
And all of it is doable with the same set of primitives, and requiring no server support (other than a node field).
Do let me know if you check it out! Or if you get stuck, happy to unblock you/clarify things (it's hard for me to know what is confusing to folks new to the project.)
Reminds me a lot of Grafast too, a new stab at this the people that made postgraphile had. I liked using graphile, haven't needed to rewrite or start a new project yet for grafast. * https://grafast.org/
Agreed on fragment masking. Graphql-codegen added support for it but in a way that unfortunately is not composable with all the other plugins in their ecosystem (client preset or bust), to the point that to get it to work nicely in our codebase we had to write our own plugins that rip code from the client preset so that we could use them as standalone plugins.
But yeah, IMO, GraphQL doesn't justify itself unless you're using a client like Relay, with data masking and fragment colocation.