arrow-return

Graphql-request vs Apollo Client: When Less Is More

Share


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.


Subscribe to
Our Newsletter

Join 1,000+ people and receive our weekly insights, tips, and best practices.