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.
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(),
})),
],
};
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.
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.
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.
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.
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.
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
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:
- Set up
provideApollo()with anInMemoryCacheand confirm a basic query renders in a component. - Build a list view whose query is exposed as a signal via
toSignal()(Section 3), including a separateloadingsignal. - Build a detail view for a single item, navigated to via routing (Week 10) with the id as a route param.
- Add a mutation reachable from the detail view (e.g. toggling a "favorite" boolean, or — against a local schema — updating a field) with an
optimisticResponseand acache.modify()update (Section 4). Confirm the UI updates instantly, before the network call resolves. - Add at least one
.graphqldocument and wire up@graphql-codegen/cli(Section 6) to generate typed operations; replace one hand-written query/type pair with the generated version.
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?
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?
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?
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 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.