Week 26: GraphQL & Apollo Angular

Week 14 built a REST data layer with HttpClient. This week adds a second one — GraphQL through Apollo Angular — and treats it as what it actually is on most teams that use it: not a wholesale replacement for REST, but a different set of tradeoffs for a specific kind of API.

Phase 7 of 8 Week 26 of 33 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Set up Apollo Angular with a typed cache and issue queries as signals via toSignal()
  • Run mutations with optimistic responses and correct cache updates
  • Generate fully-typed query/mutation functions instead of hand-writing response interfaces

1. GraphQL vs. REST, Honestly

Week 14 built one HttpClient call per resource shape the UI needed, often over- or under-fetching in the process. GraphQL flips that: the client sends a single query describing exactly the fields it wants, across however many related resources, and the server returns exactly that shape — no more, no less.

  • Genuinely better for: screens that assemble data from several related entities in one view (a dashboard combining user, orders and notifications), clients on constrained bandwidth, and APIs with many consumers that each need different subsets of the same data.
  • Not obviously better for: simple CRUD over a handful of resources, file uploads/downloads (still usually REST or signed URLs alongside GraphQL), or teams without backend buy-in to maintain a schema.
  • The honest reality on most teams: GraphQL and REST coexist. A REST endpoint for auth and file upload, GraphQL for the app's main data graph, is a completely normal split — not a migration you're expected to finish.

2. Setting Up Apollo Angular

apollo-angular wraps @apollo/client's cache and link chain behind an injectable Apollo service — conceptually the GraphQL equivalent of HttpClient, but with a normalized client-side cache built in.

app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { provideApollo } from 'apollo-angular';
import { InMemoryCache } from '@apollo/client/core';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(),
    provideApollo(() => ({
      link: /* built from HttpLink, wired to your GraphQL endpoint */ undefined as any,
      cache: new InMemoryCache(),
    })),
  ],
};
apollo.config.ts — the link, in full
import { inject } from '@angular/core';
import { HttpLink } from 'apollo-angular/http';
import { InMemoryCache, ApolloClientOptions } from '@apollo/client/core';

export function createApollo(): ApolloClientOptions<any> {
  const httpLink = inject(HttpLink);
  return {
    link: httpLink.create({ uri: '/graphql' }),
    cache: new InMemoryCache(),
  };
}

Routing the link through HttpLink means every GraphQL request also passes through Angular's HttpClient pipeline — so the functional interceptors you built in Week 14 (auth headers, error handling, logging) apply to your GraphQL traffic automatically, with no separate interceptor system to learn.

3. Queries as Signals

Apollo.watchQuery() returns an RxJS Observable that emits again whenever the cache entry for that query changes — including from a completely unrelated mutation elsewhere in the app. Bridging it with toSignal() (Week 16) turns that into ordinary template-friendly state.

order-list.ts
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { Apollo, gql } from 'apollo-angular';
import { map } from 'rxjs';

const ORDERS_QUERY = gql`
  query Orders($status: OrderStatus) {
    orders(status: $status) {
      id
      total
      placedAt
      customer { name }
    }
  }
`;

@Component({
  selector: 'app-order-list',
  templateUrl: './order-list.html',
})
export class OrderList {
  private apollo = inject(Apollo);

  private query = this.apollo.watchQuery<{ orders: Order[] }>({
    query: ORDERS_QUERY,
    variables: { status: 'PENDING' },
  });

  orders = toSignal(
    this.query.valueChanges.pipe(map((r) => r.data?.orders ?? [])),
    { initialValue: [] as Order[] },
  );

  loading = toSignal(
    this.query.valueChanges.pipe(map((r) => r.loading)),
    { initialValue: true },
  );
}

This is the same interop pattern Week 16 used for HttpClient — the only difference is that watchQuery's stream keeps emitting after the first response, because Apollo's cache, not just the network, is driving it.

Prefer rxResource() for new code

Week 16's rxResource() handles loading/error/data state as one object instead of three separate signals wired by hand, and is generally the better fit for a query like this going forward. The manual toSignal() version above is shown first because it makes explicit what's actually happening underneath.

4. Mutations & Cache Updates

A mutation without cache guidance leaves Apollo's cache stale until the next full refetch. Two ways to fix that: an optimistic response for instant UI feedback, and an explicit update function for lasting correctness.

order-list.ts — cancelling an order
const CANCEL_ORDER = gql`
  mutation CancelOrder($id: ID!) {
    cancelOrder(id: $id) {
      id
      status
    }
  }
`;

cancelOrder(order: Order) {
  this.apollo.mutate({
    mutation: CANCEL_ORDER,
    variables: { id: order.id },
    optimisticResponse: {
      cancelOrder: { __typename: 'Order', id: order.id, status: 'CANCELLED' },
    },
    update: (cache, { data }) => {
      cache.modify({
        id: cache.identify({ __typename: 'Order', id: order.id }),
        fields: { status: () => data?.cancelOrder.status },
      });
    },
  }).subscribe();
}

optimisticResponse updates the cache — and therefore every watchQuery reading that order, including the signal from Section 3 — immediately, before the network request resolves; Apollo automatically rolls it back if the real response disagrees. cache.identify() is doing the same job as a primary key would in a REST cache: telling Apollo which normalized object this response actually updates.

5. Subscriptions Over WebSockets

GraphQL subscriptions are the protocol-level answer to "push me updates as they happen" — the same problem Week 27 solves with plain HTTP via Server-Sent Events. Where SSE is a lightweight, one-directional stream you can add to any REST-ish backend, a GraphQL subscription is a persistent WebSocket connection speaking the same query language as your queries and mutations.

order-list.ts — live status updates
const ORDER_UPDATED = gql`
  subscription OnOrderUpdated($id: ID!) {
    orderUpdated(id: $id) { id status }
  }
`;

this.apollo.subscribe({
  query: ORDER_UPDATED,
  variables: { id: order.id },
}).subscribe(({ data }) => {
  // cache.modify(...) here too -- subscriptions don't auto-merge
  // into the cache the way query responses do
});

Choose based on direction and infrastructure, not preference: a subscription needs a WebSocket-capable server and link (graphql-ws) wired in alongside the HTTP link; SSE needs neither, at the cost of losing GraphQL's typed query language for that stream. Week 27 covers exactly when that tradeoff favors SSE.

6. Typed Codegen

Hand-writing a TypeScript interface next to every gql query is exactly the kind of duplication that silently drifts out of sync with the schema. @graphql-codegen/cli generates both from the schema and your .graphql documents directly.

codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
  schema: 'http://localhost:4000/graphql',
  documents: 'src/**/*.graphql',
  generates: {
    'src/app/generated/graphql.ts': {
      plugins: ['typescript', 'typescript-operations', 'typescript-apollo-angular'],
    },
  },
};

export default config;

The typescript-apollo-angular plugin goes further than plain type generation — it emits a ready-to-inject Angular service per operation (e.g. OrdersGQL), so this.ordersGQL.watch({ status: 'PENDING' }) replaces the manual Apollo.watchQuery() call from Section 3 entirely, fully typed on both variables and response.

7. Hands-on Exercise

Hands-on

Build a GraphQL-backed list + detail view with an optimistic mutation

Use any public GraphQL API for read data (e.g. a countries or Rick and Morty API), or point at a small local schema if you'd rather control the mutation side fully.

Requirements:

  1. Set up provideApollo() with an InMemoryCache and confirm a basic query renders in a component.
  2. Build a list view whose query is exposed as a signal via toSignal() (Section 3), including a separate loading signal.
  3. Build a detail view for a single item, navigated to via routing (Week 10) with the id as a route param.
  4. Add a mutation reachable from the detail view (e.g. toggling a "favorite" boolean, or — against a local schema — updating a field) with an optimisticResponse and a cache.modify() update (Section 4). Confirm the UI updates instantly, before the network call resolves.
  5. Add at least one .graphql document and wire up @graphql-codegen/cli (Section 6) to generate typed operations; replace one hand-written query/type pair with the generated version.
Hint

If the optimistic UI update "flickers" back to the old value and then forward again, your optimisticResponse shape doesn't exactly match the real mutation response shape (including __typename) — Apollo can't reconcile the two cleanly if they disagree on shape, not just on values.

8. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

What problem does GraphQL solve that a well-designed REST API doesn't already solve on its own?

Letting each client request exactly the fields and related entities it needs in a single round trip, instead of the server dictating fixed response shapes per endpoint. A REST API can partially work around this with query params or GraphQL-like composition, but it isn't the protocol's default behavior the way it is GraphQL's.

Q2

Why does routing Apollo's link through HttpLink matter for the interceptors built in Week 14?

Because HttpLink sends GraphQL requests through Angular's own HttpClient, every functional interceptor already registered for REST traffic — auth headers, error handling, logging — applies to GraphQL requests automatically, with no second interceptor mechanism to build or maintain.

Q3

What does optimisticResponse actually do, and what happens if the real response disagrees with it?

It writes a provisional result into the cache immediately, before the network request resolves, so every query reading that data (including a toSignal()-derived signal) updates instantly. If the real server response disagrees, Apollo automatically rolls back the optimistic write and applies the real one — the UI briefly shows the guess, then the truth.

Q4

When would you reach for a GraphQL subscription instead of the SSE approach from Week 27?

When the rest of your data layer is already GraphQL and the backend already supports WebSocket subscriptions — you get the same typed query language and cache integration for live data that you get for queries and mutations. If your API is otherwise plain HTTP/REST, or you just need a simple one-directional push, SSE avoids standing up WebSocket infrastructure for that alone.