Apollo Client Configuration
|
This section documents the current GraphQL specification (October 2021), plus the working draft for
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, as GraphQL tooling iterates quickly. |
Apollo Client is one of the most widely used GraphQL clients for JavaScript and TypeScript applications; this page covers how to construct one — its network layer, its normalized cache, and the APIs for reading and writing that cache directly — targeting Apollo Client v4.
ApolloClient and InMemoryCache
Every Apollo Client instance is built from two required pieces: a link that describes how requests reach the
network, and a cache that stores results and serves them back to components that ask for the same data again.
InMemoryCache is Apollo Client’s built-in cache implementation, and it is what makes the client more than a
thin fetch wrapper — it normalizes every object the client receives into a flat store keyed by identity, so
that two different queries returning the same underlying object share one cached copy rather than two unrelated
JSON blobs:
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
const client = new ApolloClient({
link: new HttpLink({ uri: 'https://api.example.com/graphql' }),
cache: new InMemoryCache(),
});
This minimal setup is enough to issue queries and mutations, but production applications almost always need more: authentication headers, retry behavior, subscriptions over a different transport, and cache normalization rules tuned to the schema. The rest of this page builds each of those pieces up from this starting point. See Apollo Client — Caching overview for the cache’s own introduction, which this section assumes as background.
HttpLink: endpoint, headers, and credentials
HttpLink is the terminating link that actually sends a request over HTTP; it is the network transport at the
bottom of every link chain described later on this page. Beyond the endpoint uri, it accepts static or
per-request headers and the credentials mode a browser should use for cookies on cross-origin requests:
import { HttpLink } from '@apollo/client';
const httpLink = new HttpLink({
uri: 'https://api.example.com/graphql',
headers: {
'X-Client-Name': 'storefront-web',
},
credentials: 'include', // send cookies on cross-origin requests
});
Static headers set here apply to every request sent through this link. A header that depends on per-request
state — most commonly an auth token that can expire and refresh mid-session — belongs in an ApolloLink placed
before HttpLink in the chain instead, covered next.
ApolloLink chains
ApolloLink is Apollo Client’s middleware abstraction: each link in a chain can inspect or modify a request
before it moves on, and inspect or modify a response before it moves back up. Links compose left to right with
the static ApolloLink.from, terminating in a link that actually performs the network call (HttpLink, or the
GraphQLWsLink covered under subscriptions below):
import { ApolloLink, HttpLink, InMemoryCache, ApolloClient, CombinedGraphQLErrors } from '@apollo/client';
import { ErrorLink } from '@apollo/client/link/error';
import { RetryLink } from '@apollo/client/link/retry';
import { BatchHttpLink } from '@apollo/client/link/batch-http';
import { SetContextLink } from '@apollo/client/link/context';
const httpLink = new BatchHttpLink({ uri: 'https://api.example.com/graphql' });
const authLink = new SetContextLink((_prevContext, { headers }) => {
const token = getStoredAccessToken();
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
};
});
const errorLink = new ErrorLink(({ error }) => {
if (CombinedGraphQLErrors.is(error)) {
error.errors.forEach(({ message, path }) =>
console.error(`[GraphQL error] ${message}, path: ${path}`),
);
} else if (error) {
console.error(`[Network error] ${error.message}`);
}
});
const retryLink = new RetryLink({
delay: { initial: 300, max: 5000, jitter: true },
attempts: { max: 3, retryIf: (error) => !!error },
});
const link = ApolloLink.from([errorLink, retryLink, authLink, httpLink]);
const client = new ApolloClient({ link, cache: new InMemoryCache() });
Each link in this chain has a distinct job:
-
The auth link (
SetContextLink) attaches anAuthorizationheader from whatever token store the application uses, re-read on every request rather than baked in once at startup. -
The
ErrorLinkcentralizes error observation — logging, reporting, or reacting to a specific error code — for both GraphQL errors and lower-level network/server errors, surfaced through the singleerrorproperty (narrowed withCombinedGraphQLErrors.is,ServerError.is, and similar checks) rather than separategraphQLErrors/networkErrorfields, without every call site needing its own try/catch. -
RetryLinkretries a failed operation with a configurable delay and backoff, useful for transient network failures;retryIfcontrols which errors are worth retrying at all (a400from a malformed query usually is not). -
BatchHttpLinkreplacesHttpLinkas the terminating link when several operations fired in the same event loop tick should be combined into a single HTTP request, trading a small batching delay for fewer round trips under bursty query patterns.
Order matters: a link only sees what the links before it in the ApolloLink.from array pass along, so the
auth link must run before the terminating HTTP link actually sends the request, and the error link is typically
placed early so it can observe everything that happens further down the chain. See
Apollo Client — Advanced HTTP
networking for the full set of built-in links and their options.
Splitting to a subscriptions transport
Queries and mutations travel over HTTP, but GraphQL subscriptions need a persistent connection, typically
graphql-ws over a WebSocket. ApolloLink.split routes an operation to one link or another based on a
predicate evaluated against the parsed query document — here, whether the operation is a subscription:
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { ApolloLink, HttpLink, InMemoryCache, ApolloClient } from '@apollo/client';
import { getMainDefinition } from '@apollo/client/utilities';
const wsLink = new GraphQLWsLink(
createClient({ url: 'wss://api.example.com/graphql' }),
);
const httpLink = new HttpLink({ uri: 'https://api.example.com/graphql' });
const splitLink = ApolloLink.split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink,
);
const client = new ApolloClient({ link: splitLink, cache: new InMemoryCache() });
The authLink/errorLink/retryLink chain from the previous section can wrap either branch of the split,
most commonly the HTTP branch, since a graphql-ws connection typically authenticates once at connection time
via connectionParams rather than per-message headers.
Subscriptions covers the graphql-ws protocol itself — the
connection lifecycle, keep-alives, and per-subscription message shape — independent of any one client library.
Fetch policies
A fetch policy controls how a query balances the normalized cache against the network on each execution.
InMemoryCache makes cached data available instantly, but a policy still has to decide whether that cached data
is trustworthy enough to serve without checking the network first:
| Policy | Behavior |
|---|---|
|
Default. Read the cache; only hit the network if the requested data is missing or incomplete. |
|
Read the cache immediately for a fast first render, but always also issue a network request and update once it resolves. |
|
Always hit the network; still write the result into the cache afterward. |
|
Never hit the network; return whatever the cache has (or nothing). |
|
Always hit the network, and never read from or write into the cache at all. |
|
Like |
const { data, loading } = useQuery(GET_BOOK, {
variables: { id: '42' },
fetchPolicy: 'cache-and-network',
});
cache-and-network is a common choice for views that should paint instantly from cache while still reconciling
against the latest server state, at the cost of a network request that may re-render the component a second
time once it resolves. no-cache is useful for one-off queries whose result should never pollute the shared
normalized store — a search-as-you-type query, for example, where transient results have no business living
alongside durable domain objects.
typePolicies, keyFields, and field policies
InMemoryCache normalizes an object into the flat store using a cache identifier built from its __typename
plus an identifying field, id by default. typePolicies lets that default be overridden per type — naming a
different key field, disabling normalization for a type entirely, or attaching a field policy that
customizes how one specific field reads and merges its cached value:
const cache = new InMemoryCache({
typePolicies: {
Book: {
keyFields: ['isbn'], // use isbn instead of id as this type's cache key
},
Query: {
fields: {
// relayStylePagination merges paginated pages of a Relay-style connection
// into one growing list under a single cache entry, keyed by field arguments.
books: relayStylePagination(),
},
},
},
});
keyFields is the most common override: a type that is identified by something other than id — an ISBN, a
composite key, or a value nested one level down (keyFields: ['author', 'id']) — needs it spelled out
explicitly, or InMemoryCache falls back to treating every instance as a distinct, unnormalized object. A field
policy’s merge function controls how a newly fetched value for a field combines with whatever value is
already cached there; relayStylePagination() is a ready-made field policy that implements this merge logic for
a Connection-shaped field so that fetching page two appends onto the already-cached page one rather than
replacing it. See Pagination for the Connection/Edge/PageInfo shape
this helper assumes, and
Apollo Client — Pagination for
relayStylePagination and the other built-in pagination field policies.
Reading and writing the cache directly
Most cache interaction happens implicitly through useQuery and useMutation, but three APIs read or write the
normalized store directly, most often from inside a mutation’s update callback so a related query re-renders
without waiting on a fresh network round trip:
// cache.modify: update one field of an already-cached object in place
cache.modify({
id: cache.identify({ __typename: 'Book', id: '42' }),
fields: {
rating(existingRating) {
return existingRating + 1;
},
},
});
// writeQuery: seed or overwrite the result of a whole query
cache.writeQuery({
query: GET_BOOK,
variables: { id: '42' },
data: { book: { __typename: 'Book', id: '42', title: 'Dune' } },
});
// updateQuery: read a query's current cached result and derive a new one from it
cache.updateQuery({ query: GET_BOOKS }, (existing) => ({
books: [...(existing?.books ?? []), newBook],
}));
cache.modify is the finest-grained tool — a single field on a single normalized object — and is the usual
choice after a mutation that changes one property of an entity already in the store. writeQuery replaces an
entire query’s cached result wholesale, useful for seeding the cache from data obtained outside Apollo Client
entirely (a server-rendered payload, for instance). updateQuery reads the current result and returns a new one
derived from it, which is the safer choice when the update depends on what is already cached — appending to a
list, for example — rather than overwriting unconditionally.
The persisted-queries link
PersistedQueryLink replaces a full query document in the request body with a short hash on repeat calls,
after the server has confirmed it already knows that hash; a full miss falls back to sending the complete query
once so the server can register it. It sits in front of HttpLink in the chain:
import { ApolloLink } from '@apollo/client';
import { PersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash';
const persistedQueriesLink = new PersistedQueryLink({ sha256 });
const link = ApolloLink.from([persistedQueriesLink, httpLink]);
This is the client half of automatic persisted queries; Caching covers why a
persisted-query hash is also useful as an HTTP cache key on the server and CDN side of the same mechanism, since
a hash — unlike a full query body — fits into a cacheable GET URL.
SSR hydration
In a server-rendered application, the server runs queries during rendering and can serialize the resulting cache
contents into the HTML it sends down, so the client doesn’t have to re-fetch data it already has. The client
constructs its own InMemoryCache and restores that serialized state before the application renders:
// Server: after rendering, extract the populated cache as plain data
const initialState = client.extract();
// ...embed `initialState` into the HTML response as a serialized script tag...
// Client: restore that data into a fresh cache before the app hydrates
const cache = new InMemoryCache().restore(window.__APOLLO_STATE__);
const client = new ApolloClient({ link, cache });
restore seeds the normalized store directly, so a query whose result was already fetched on the server renders
immediately on the client without a duplicate network request, as long as the query’s cache key — shaped by the
same typePolicies described above — matches between server and client.
A normalized cache in practice
InMemoryCache stores every normalized object once, under a key built from its __typename and identifying
field, and references it by that key from wherever it appears in a query result. Two independent queries that
each select an overlapping object share the same cached entry rather than two separate copies:
query BookDetail {
book(id: "42") {
id
title
rating
}
}
query LibraryShelf {
shelf(id: "sf-classics") {
id
books {
id
title
rating
}
}
}
Assuming shelf(id: "sf-classics") includes the same book, the normalized store after both queries have run
looks like this — one Book:42 entry, referenced from two places:
{
"Book:42": { "__typename": "Book", "id": "42", "title": "Dune", "rating": 5 },
"ROOT_QUERY": {
"book({\"id\":\"42\"})": { "__ref": "Book:42" }
},
"Shelf:sf-classics": {
"__typename": "Shelf",
"id": "sf-classics",
"books": [{ "__ref": "Book:42" }]
}
}
Because both the book query and the shelf query’s books list point at the same Book:42 entry, updating
that entry once — through cache.modify, or as the side effect of a mutation’s own response — is visible
immediately in both places the book is displayed, with no extra network request and no risk of the two views
drifting out of sync. This is the same normalization principle
Global Object Identification describes from the
server’s side: InMemoryCache can only build cache keys like Book:42 because every Book carries a stable,
globally meaningful id in the first place.
Related pages
-
Clients overview surveys Apollo Client alongside urql, Relay, and the JVM/Python clients before this page’s configuration detail.
-
Other clients covers urql, Relay, and the JVM/Python clients this page does not.
-
Pagination covers the
Connection/Edge/PageInfoshape thatrelayStylePaginationconsumes. -
Caching covers persisted queries and HTTP caching from the server and CDN side of the mechanisms configured on this page.
-
Global Object Identification covers the server-side
idguarantees that make cache normalization possible at all. -
Subscriptions covers the
graphql-wsprotocol underlying theGraphQLWsLinkbranch of thesplitabove.