Why I Stopped Writing GraphQL
I shipped GraphQL on three production services. I'd ship REST on all three today.
I was a GraphQL believer for years. Five years of production experience later, I've stopped reaching for it. The trade-offs that looked great on paper haven't held up in maintenance.
I shipped my first GraphQL service in 2019. By 2022 I was running three. In 2024 I migrated all three back to REST or RPC. This is what I learned, and why I now default away from GraphQL on new projects.
What I thought GraphQL gave me
The pitch was good and I bought it:
- One endpoint, no over-fetching.
- A type-safe schema shared between frontend and backend.
- Versionless evolution: add fields, deprecate fields, never break the contract.
- Better tooling for client developers.
For about two years, those promises mostly held. Then the cracks appeared.
Crack 1: N+1 will eat you alive
GraphQL's join story is DataLoader, and DataLoader works, but only if every resolver carefully participates. In a team of 10 engineers, someone forgets. The result is a query that reads 4,000 rows from the database, one at a time. The frontend is happy. The database catches fire.
I had to build N+1 detection in CI to catch this. We caught it. But the engineering effort to build the detection was about three weeks of senior time.
In REST, the equivalent bug is much harder to write because you have to actively design an endpoint that does the wrong thing. In GraphQL, you can write the wrong thing by accident in any resolver.
Crack 2: Authorization gets harder, not easier
In REST, I have a single endpoint and a single authz check. In GraphQL, every field is potentially a separate authorization concern. Should this user see this email field on this customer? Should this admin see that internal note? You end up with field-level authorization logic, and it spreads.
A frontend engineer can compose a query that touches 30 fields. Each field has to evaluate auth. The reasoning becomes very hard to audit.
In banking, this would never have passed compliance review.
Crack 3: Caching is genuinely worse
HTTP caching is one of the great inventions of the web. ETags, cache-control, CDNs, all of it works because the URL is the cache key.
A GraphQL POST with a body of {"query": "..."} is uncacheable by every layer between you and the database. You can build query-aware caches, and tools like Apollo's persisted queries help, but you've now reinvented HTTP caching at the application layer.
Crack 4: The "versionless API" myth
The promise was that you'd never need v2. In practice, when business logic changes, you do need a new contract, and you end up with parallel fields like name and displayName and displayNameV2. That's not versionless. That's versioning by inflation.
REST with Stripe-style versioning is, in practice, easier to evolve.
Crack 5: It's harder to debug
A REST 500 error is a single endpoint that failed. I look at the logs, I find the request, I move on. A GraphQL query that touched seven resolvers, of which two failed, returns a partial response with errors mixed in. Pinpointing the failing resolver in production logs is genuinely harder.
This sounds minor. Across hundreds of incidents, it adds up.
What I do now
For most APIs I build today:
- Internal service-to-service: gRPC with protobuf. Strongly typed, fast, codegen for clients.
- Public APIs: REST with JSON, version-pinned per account, idempotency keys, errors with type/code/doc URL. The Stripe model.
- Frontend-to-backend on a Next.js app: server actions or tRPC, depending on team preference.
I have not reached for GraphQL on a greenfield project in two years.
When GraphQL is still right
I'm not anti-GraphQL on principle. There are real cases where it's the right answer:
- You have many client teams pulling different shapes of data. Spotify and GitHub use GraphQL because hundreds of independent clients consume their data.
- The schema itself is the product. If you're building a developer platform, the schema is genuinely valuable.
- Your team has deep GraphQL operational expertise. This is a real moat. If you have it, use it.
For most startups, none of those apply.
The team-size factor
GraphQL's payoff scales with the number of consuming clients. With one client (your web app), GraphQL is mostly cost. With 10 clients (mobile, web, partner integrations, internal tools), GraphQL might pay for itself. With 100 clients, it almost certainly does.
Most of the startups I work with have one or two clients. Building GraphQL for one client is over-engineering with a long maintenance tail.
The sharper insight
GraphQL was sold as a frontend convenience and turned out to be a backend cost. Every engineering org I know that adopted GraphQL eventually built a GraphQL platform team. None of those teams existed before adoption. That hidden tax never shows up in the original decision document.
If you don't have the spare capacity for a platform team, you cannot afford GraphQL. REST will treat you better.
For more on the API design patterns I do reach for, see reading Stripe's source code.