Apollo Client Configuration

This section documents the current GraphQL specification (October 2021), plus the working draft for @defer/@stream/@oneOf, and the GraphQL-over-HTTP specification, as published at graphql.org and spec.graphql.org — which are the references these pages are written and verified against — and, for the integration pages, against Spring for GraphQL (2.0.x), Strawberry, Ariadne, and the client docs for Apollo Client / urql / Relay. No specific patch version is pinned. Some surfaces (Apollo Router/managed federation, graphql-ws internals, the Relay compiler internals, GraalVM native) are linked rather than documented in depth.

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 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 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 an Authorization header from whatever token store the application uses, re-read on every request rather than baked in once at startup.

  • The ErrorLink centralizes error observation — logging, reporting, or reacting to a specific error code — for both GraphQL errors and lower-level network/server errors, surfaced through the single error property (narrowed with CombinedGraphQLErrors.is, ServerError.is, and similar checks) rather than separate graphQLErrors/networkError fields, without every call site needing its own try/catch.

  • RetryLink retries a failed operation with a configurable delay and backoff, useful for transient network failures; retryIf controls which errors are worth retrying at all (a 400 from a malformed query usually is not).

  • BatchHttpLink replaces HttpLink as 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

cache-first

Default. Read the cache; only hit the network if the requested data is missing or incomplete.

cache-and-network

Read the cache immediately for a fast first render, but always also issue a network request and update once it resolves.

network-only

Always hit the network; still write the result into the cache afterward.

cache-only

Never hit the network; return whatever the cache has (or nothing).

no-cache

Always hit the network, and never read from or write into the cache at all.

standby

Like cache-first, but does not trigger an automatic network fetch even when the query is stale.

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.

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.

  • 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/PageInfo shape that relayStylePagination consumes.

  • 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 id guarantees that make cache normalization possible at all.

  • Subscriptions covers the graphql-ws protocol underlying the GraphQLWsLink branch of the split above.