We recently migrated a large client portal from Apollo Client to graphql-request — and the results were immediate: smaller bundle, less boilerplate, faster onboarding for new developers.
But this isn't a "just switch" story. It's a "know your app's shape first" story.
Why We Left Apollo Client
The portal in question was content-facing and read-heavy — mostly public pages pulling data once and rendering it. Classic editorial product. Apollo Client was already in place when the project landed on our desk, and on paper it made sense. In practice, we were paying for a lot of horsepower we never used.
Here's what Apollo was bringing to the table that the project didn't need:
Normalized cache — powerful for apps where the same entity appears in many places at once, but overkill when content is fetched once and rendered once
Reactive cache system — adds real complexity when there's little shared mutable state across components
Bundle weight — Apollo Client is not small; every user was downloading functionality the app never used
Boilerplate — providers, cache config, type policies — significant setup for what amounted to "fetch data, render it, move on"
The turning point was simple: maintenance overhead no longer justified actual feature usage. That's when a migration makes sense.
Why graphql-request Won for This Project
graphql-request does one thing: sends a GraphQL query and returns the result. No magic, no abstraction layers, no setup ceremony. That turned out to be exactly right for this portal.
The concrete wins after migrating:
Smaller bundle — graphql-request is a fraction of Apollo Client's size; users download less, which can improve load performance, especially on content-heavy pages
Dead simple API — one function call replaced providers, hooks, and cache configuration
Zero learning curve — any developer on the team can read a graphql-request call and understand it in under a minute
Faster onboarding — new developers contributed to the codebase on day one, without needing to learn Apollo's mental model first
These aren't minor quality-of-life improvements. On a content portal with a lean team and delivery pressure, they directly impacted velocity.
The Honest Trade-Off
Let's be clear: graphql-request is not a drop-in replacement for Apollo everywhere. Choosing it for this project was right precisely because the project's shape matched what it offers. A different project shape leads to a different answer.
The team behind Graffle (graphql-request's successor library, currently in pre-release) is explicit about this: Apollo, Urql, and Relay are UI-specialized clients built for deep framework integration. graphql-request is a general-purpose client optimized for type safety and simplicity — not for the reactive, mutation-heavy patterns common in admin tools and dashboards.
That distinction matters a lot in practice.
Where Apollo Client Still Wins
If your project has any of the following characteristics, Apollo's extra weight starts paying for itself quickly.
Shared entities across multiple views
Apollo's normalized cache automatically deduplicates and synchronizes entities across every component that queries them. In an admin panel where a user record appears in a table, a detail view, and a sidebar simultaneously, update it once and every view re-renders with fresh data. graphql-request has no cache at all — every request goes to the network, and there's no shared store keeping views in sync.
Optimistic UI
Apollo supports optimistic updates out of the box: update the UI immediately when a mutation fires, roll back automatically if the server rejects it. For admin tools where users expect instant feedback on edits, toggles, and inline changes, this is table stakes. With graphql-request, you build this yourself — mutation by mutation.
Complex mutation patterns
Apollo's refetchQueries, cache eviction, and update functions give you fine-grained control over what re-fetches after a mutation. In a CMS or admin interface where mutations are constant — creating, editing, archiving records — keeping every dependent view in sync without a cache layer becomes a manual, error-prone job. Apollo handles this gracefully; graphql-request leaves it entirely to you.
File uploads
Apollo has a well-established ecosystem solution for uploads, such as apollo-upload-client, which integrates cleanly with the GraphQL multipart request spec. graphql-request has no dedicated upload package or built-in multipart support. Since it's a thin wrapper around fetch, uploading files means manually implementing the multipart spec yourself. For admin panels and CMS tools — where uploading media, documents, and assets is a core feature, not an edge case — this is a meaningful gap.
Deep framework integration
Apollo's React hooks (useQuery, useMutation) are built for component-level reactivity: loading states, error states, and automatic re-renders come included. graphql-request returns a plain promise. You build the loading and error state management yourself, every time, for every component.
A Quick Note on Graffle
graphql-request has been renamed to Graffle — an evolved version from the same author, same codebase lineage. The original graphql-request still lives on its own branch for teams who want the simpler, stable API without adopting a new library.
Graffle adds meaningful improvements: a document builder with full type inference, an extension system, multi-transport support. But it's currently in pre-release, which means APIs may still shift before a stable 1.0 release.
Practical takeaway: if you're choosing today, graphql-request (stable) is the safer pick for the minimal-client use case. Graffle is worth watching — just not yet a default recommendation for production.
How to Choose: A Quick Decision Framework
Run your project through this checklist before committing to either tool:
Mostly read-heavy and content-facing? → graphql-request fits well
State mostly local to each page, minimal shared entities? → graphql-request fits well
Bundle size and simplicity are top priorities? → graphql-request fits well
Same entities appearing across multiple views that need to stay in sync? → lean Apollo
Need optimistic UI for instant feedback on mutations? → lean Apollo
Frequent mutations with complex cache invalidation? → lean Apollo
File and media uploads as a core feature? → lean Apollo
Admin panel, dashboard, or CMS-heavy interface? → Apollo's extra weight is probably worth it
The pattern is consistent: the more your app writes, mutates, and keeps multiple views in sync, the more Apollo earns its place. The more your app reads, renders, and moves on, the more Apollo gets in the way.
Conclusion
The migration was the right call — for this specific portal. The results backed it up. But the lesson isn't "go minimal." It's “match the client to the shape of the app”.
Admin panels and CMS-heavy products lean on exactly the features graphql-request deliberately leaves out: caching, optimistic updates, upload handling, framework integration. Content portals and read-heavy products rarely need any of them.
Picking a GraphQL client isn't about which one is "better." It's about which one matches what you're actually building.