{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreifnl27uu7xu6eorfcmcicokvo42hydqpomfddi4gyzpqsfy6ytvw4",
"uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mpek47sgtv42"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreifzsx7oov2c6mg76j25pyxcgaq4v5uplc4celjxksq36fi7ysj2sa"
},
"mimeType": "image/webp",
"size": 72440
},
"path": "/p7x-dev/i-got-tired-of-apollo-angular-bugs-and-built-my-own-graphql-client-5bpk",
"publishedAt": "2026-06-28T17:36:34.000Z",
"site": "https://dev.to",
"tags": [
"webdev",
"angular",
"graphql",
"Issue #2371",
"Issue #8958",
"9319",
"10289",
"9735",
"11804",
"8958",
"2371",
"2414",
"668",
"3877",
"3406",
"183",
"https://github.com/DumbGQL/dumbql",
"@apollo",
"@dumbql",
"@angular",
"@Component"
],
"textContent": "For the past year, I've been building Angular applications that talk to GraphQL backends. And for the past year, I've been fighting Apollo Angular.\n\nNot fighting GraphQL. GraphQL itself is great. But every client available for Angular is either a React port with Angular bolted on, React-only entirely, or so opinionated it forces you to restructure your backend.\n\nSo I built my own. It's called **DumbQL**. This is the story of why, and what I learned.\n\n## The Problem with Existing Solutions\n\nLet me be concrete about what I mean by \"fighting Apollo Angular.\"\n\n### Apollo Angular: A React Client Wearing Angular Clothes\n\nApollo Angular is a wrapper around `@apollo/client`, which is fundamentally a React library. This creates a cascading set of problems:\n\n * **Angular version always lags React.** When `@apollo/client` v4.0 dropped, Apollo Angular stayed stuck on v3 compatibility. Issue #2371 captures this — it sat open for months with a maintainer comment essentially saying \"I'll get to it eventually.\"\n * **`react` is a dependency even in non-React projects.** Install Apollo Angular, look at your `node_modules` — React is there. Always. Because `@apollo/client` has it as a peer dependency. Issue #8958.\n * **Signals? Forget it.** Angular 17+ made signals the core reactive primitive. Apollo Angular has no native signals support. You're on your own to bridge the gap.\n\n\n\n### The Cache Problem\n\nApollo's normalized cache is powerful. It's also a source of endless pain.\n\nEvery type in your schema needs `typePolicies`. Every mutation needs a manual `update` or `refetchQueries`. Forget one, and you have stale UI in production.\n\n\n\n // Apollo: you write this for every type, forever\n new InMemoryCache({\n typePolicies: {\n User: { keyFields: ['id'] },\n Post: { keyFields: ['id'] },\n Comment: { keyFields: ['id'] },\n // ... and so on\n }\n })\n\n\nAnd even when you set it up correctly, there are production bugs that have been open for years:\n\n * #9319 — `INVALIDATE` in `cache.modify` silently does nothing. Stale data persists with no refetch.\n * #10289 — `cache.evict` no-ops inside optimistic UI. Open since 2022.\n * #9735 — Internal results cache merges stale data into `readFromStore` in production only.\n\n\n\n### URQL: Doesn't Support Angular\n\nURQL is actually well-architected. The exchange model is clean. But it's React-only. There's no Angular binding and no plans for one.\n\n### Relay: Great if Your Backend Speaks Relay\n\nRelay is the most opinionated of the three. It requires your backend to implement the `Node` interface, the `Connection` spec for pagination, and a compiler build step. If you're not building a Meta-style architecture from scratch, Relay is off the table.\n\nAnd it's React-only anyway.\n\n## So I Started with HttpClient\n\nI wasn't planning to build a GraphQL client. I was planning to build a frontend.\n\nI started with Angular's built-in `HttpClient` to make GraphQL requests. It's actually fine for basic usage:\n\n\n\n this.http.post<{ data: { getUser: User } }>('/graphql', {\n query: `{ getUser { id name email } }`\n }).pipe(map(r => r.data.getUser))\n\n\nBut then I needed caching. Then auth token refresh. Then file uploads. Then I wanted proper TypeScript types for my queries. Then I wanted DevTools to inspect what was happening.\n\nEach one I added myself. And at some point I realized I had a GraphQL client.\n\n## What DumbQL Is\n\nDumbQL is a modular GraphQL client suite built Angular-native from day one. The core is built on `HttpClient`. Everything else is opt-in.\n\n> Too dumb to be complex. Too smart to repeat the same mistakes.\n\n### The Core Philosophy: Opt-In Everything\n\n\n @dumbql/core ~10KB — the minimum viable GraphQL client\n @dumbql/cache ~3KB — normalized cache, only if you want it\n @dumbql/middlewares ~3KB — auth refresh, retry, offline queue\n @dumbql/subscriptions ~2KB — WebSocket via graphql-transport-ws\n @dumbql/pagination ~2KB — cursor + offset helpers\n @dumbql/file-upload ~1KB — multipart uploads\n @dumbql/ssr ~1KB — TransferState for Angular Universal\n @dumbql/testing ~1KB — mock backend for unit tests\n @dumbql/debugging ~2KB — operation recording + DevTools\n @dumbql/codegen — — TypeScript types from your schema\n @dumbql/downloader ~1KB — schema introspection CLI\n @dumbql/fragments ~1KB — type-safe fragment utilities\n @dumbql/persisted-queries ~1KB — APQ with SHA-256\n\n\nYou don't pay for what you don't use. Every package is `sideEffects: false`. If you only need `@dumbql/core`, that's all that's in your bundle.\n\n### Setup\n\n\n // app.config.ts\n import { provideDumbql } from '@dumbql/core';\n import { provideHttpClient } from '@angular/common/http';\n\n export const appConfig = {\n providers: [\n provideHttpClient(),\n provideDumbql({\n endpoint: 'http://localhost:4000/graphql',\n }),\n ],\n };\n\n\nThat's it. No cache configuration. No link chain. No provider tree.\n\n### Usage\n\n\n import { Component, inject } from '@angular/core';\n import { GraphqlService, gql } from '@dumbql/core';\n import { map } from 'rxjs';\n\n const GET_USER = gql`{ getUser { id name email } }`;\n\n @Component({\n standalone: true,\n template: `<div>{{ (user$ | async)?.name }}</div>`,\n })\n export class UserComponent {\n private gql = inject(GraphqlService);\n\n user$ = this.gql.query(GET_USER).pipe(\n map(r => r.status === 'success' ? r.data.getUser : null),\n );\n }\n\n\n## The Cache Problem, Solved\n\nThe Apollo cache requires `typePolicies` for every type. DumbQL's cache requires nothing.\n\n\n\n // DumbQL: this is the entire cache configuration\n provideDumbql({\n endpoint: '/graphql',\n cache: { enabled: true }\n })\n\n\nUnder the hood, `@dumbql/cache` uses `__typename` + `id` (or `_id`) auto-detection to normalize entities. Every response is walked recursively. Entities are extracted and stored keyed by `__typename:id`. Mutations automatically evict related cache keys.\n\nNo `cache.modify`. No `refetchQueries`. No stale UI.\n\nIf you need advanced behavior, `typePolicies` are available — but you don't start there.\n\n\n\n cache: {\n enabled: true,\n typePolicies: {\n PaginatedResult: { merge: 'append' }, // only when you need it\n }\n }\n\n\n## Middlewares: Everything Apollo Needs Third-Party Packages For\n\nApollo needs external packages for auth refresh, file uploads, persisted queries, and offline support. DumbQL ships all of these as first-party `@dumbql/*` packages with a unified config.\n\n### Auth Token Refresh\n\n\n provideDumbql({\n endpoint: '/graphql',\n middlewares: {\n authRefresh: {\n enabled: true,\n refreshEndpoint: '/auth/refresh',\n triggerStatuses: [401],\n headerName: 'Authorization',\n }\n }\n })\n\n\nRequests that arrive during a token refresh are queued and replayed automatically.\n\n### Offline Queue\n\n\n provideDumbql({\n endpoint: '/graphql',\n middlewares: {\n offlineQueue: {\n enabled: true,\n maxQueueSize: 50,\n persistQueue: true, // survives page reload\n }\n }\n })\n\n\nMutations made while offline are queued in `localStorage` and replayed on reconnect.\n\n### Retry with Exponential Backoff\n\n\n provideDumbql({\n endpoint: '/graphql',\n retryCount: 3,\n retryDelay: 1000, // 1s, 2s, 4s...\n })\n\n\n## Angular-Native Features\n\n### Signals\n\nDumbQL works with Angular signals out of the box since it's built on `HttpClient` and RxJS — the same primitives Angular's signal APIs interop with.\n\n### Router Integration\n\n\n // Guarded routes that wait for GraphQL data\n export const routes: Routes = [\n {\n path: 'profile',\n ...guardedRoute(GET_USER, {\n redirect: '/login',\n check: r => r.status === 'success' && !!r.data.getCurrentUser,\n }),\n component: ProfileComponent,\n }\n ];\n\n\n### Angular Pipes\n\n\n <!-- Extract data or null on error -->\n <div>{{ result | graphqlData | json }}</div>\n\n <!-- Extract error string or null on success -->\n <div *ngIf=\"result | graphqlError as err\">{{ err }}</div>\n\n\n### `ng add` Schematics\n\n\n ng add @dumbql/core\n\n\nInteractive prompts generate a typed `dumbql.config.ts` for your project.\n\n## Type Safety\n\nDumbQL ships `TypedDocumentNode<TResult, TVars>` with phantom types, so your query results are fully typed without a build step.\n\nYou can also use `@dumbql/codegen` to generate TypeScript interfaces directly from your GraphQL schema:\n\n\n\n npm run schema:download # introspection → schema.json + schema.graphql\n npm run codegen # schema → TypeScript interfaces\n\n\n\n // graphql/types/index.ts (generated)\n export interface User { id: string; username: string; email: string; }\n export interface Query { getCurrentUser: User; getUsers: User[]; }\n\n\n## Error Handling\n\nApollo's `errorPolicy` has a type-narrowing problem — `data` can be `undefined` even on success. DumbQL uses a discriminated union:\n\n\n\n service.query<{ user: User }>(GET_USER).subscribe(result => {\n if (result.status === 'success') {\n // result.data is User — fully typed, never undefined\n console.log(result.data.user);\n } else {\n // result.error is string\n console.error(result.error);\n }\n });\n\n\nHelper functions available: `isSuccess`, `isError`, `unwrap`, `unwrapOrThrow`, `mapResult`.\n\n## DevTools\n\nDumbQL ships a browser extension for Chrome and Firefox. It connects to `devtoolsMiddleware` and shows:\n\n * Real-time request log with timing\n * Schema visualization (SDL tree)\n * Field tree inspector per query\n * Entity cache browser\n * Mutation timing charts\n\n\n\n\n provideDumbql({\n endpoint: '/graphql',\n devtools: { autoConnect: true, maxRequests: 500 }\n })\n\n\n## Testing\n\n\n import { MockGraphqlService, provideDumbqlTesting } from '@dumbql/testing';\n\n TestBed.configureTestingModule({\n providers: [\n provideHttpClientTesting(),\n provideDumbqlTesting(),\n ],\n });\n\n const mock = TestBed.inject(MockGraphqlService);\n mock.when(GET_USER, {\n status: 'success',\n data: { user: { id: '1', name: 'Test' } }\n });\n\n\nSimple `when(query, result)` API. FIFO queue. Optional simulated network delay.\n\n## Bugs Fixed from Other Clients\n\nThese are real GitHub issues that DumbQL addresses by design:\n\nProject | Issue | Problem\n---|---|---\nApollo | #9319 | `INVALIDATE` silently no-ops — stale data persists\nApollo | #10289 | `cache.evict` no-ops inside optimistic UI (open since 2022)\nApollo | #11804 | Skipped query ignores `clearStore()` — returns outdated data\nApollo | #9735 | Production-only: internal cache merges stale data\nApollo | #8958 | `react` required as dependency in non-React projects\nApollo Angular | #2371 | Angular version lags React — incompatible with `@apollo/client` v4\nURQL | #2414 | `relayPagination` shows stale data when non-relay params change\nURQL | #668 | Query doesn't refetch when variables change\nURQL | #3877 | Pages concatenate in write order — flickering mis-ordered items\nRelay | #3406 | React-only — no Angular, Vue, or Svelte support\nRelay | #183 | Forces `Node` interface + `Connection` spec on your backend\n\n## What's Next\n\nDumbQL started Angular-native. But the core is framework-agnostic — `@dumbql/core` has zero framework dependencies.\n\nReact and Vue bindings are in progress. The plan is the same opt-in model: `@dumbql/react` and `@dumbql/vue` as thin adapter layers over the same middleware pipeline and cache.\n\nThe goal isn't to replace Apollo everywhere. The goal is to give Angular developers a first-class GraphQL client that wasn't designed for a different framework first.\n\n## Try It\n\n\n npm install @dumbql/core\n\n\nOr with schematics:\n\n\n\n ng add @dumbql/core\n\n\nGitHub: https://github.com/DumbGQL/dumbql\n\nFull comparison table, architecture diagram, and configuration reference in the README.\n\n_DumbQL is MIT licensed and actively developed. Issues and PRs welcome._",
"title": "I got tired of Apollo Angular bugs and built my own GraphQL client"
}