Setting up Apollo Client for GraphQL in Web Applications
Apollo Client is a complete GraphQL client with normalized cache, reactive queries, mutations, subscriptions, and auto-generation of types. The cache stores data by object identifiers — updating one record automatically updates all components that use it.
Works with React, Vue, Angular — it's a standalone library. Particularly beneficial in applications with many interconnected objects.
What's Included
Installing and configuring Apollo Client, setting up HTTP and WebSocket links, authentication, cache policies, code generation of types from GraphQL schema, hooks for queries/mutations/subscriptions, error handling, DevTools.
Installation
npm install @apollo/client graphql
# for WebSocket
npm install graphql-ws
Client Configuration
// lib/apollo/client.ts
import {
ApolloClient,
InMemoryCache,
createHttpLink,
from,
ApolloLink,
} from '@apollo/client'
import { setContext } from '@apollo/client/link/context'
import { onError } from '@apollo/client/link/error'
import { GraphQLWsLink } from '@apollo/client/link/subscriptions'
import { createClient as createWsClient } from 'graphql-ws'
import { getMainDefinition } from '@apollo/client/utilities'
import { split } from '@apollo/client'
const httpLink = createHttpLink({
uri: import.meta.env.VITE_GRAPHQL_URL ?? '/graphql',
})
// add Authorization header to each request
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('token')
return {
headers: {
...headers,
...(token ? { authorization: `Bearer ${token}` } : {}),
},
}
})
// error handling
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
for (const { message, locations, path, extensions } of graphQLErrors) {
console.error(`[GraphQL error]: ${message}`, { locations, path })
if (extensions?.code === 'UNAUTHENTICATED') {
authStore.logout()
}
}
}
if (networkError) {
console.error('[Network error]', networkError)
}
})
// WebSocket for subscriptions
const wsLink = new GraphQLWsLink(
createWsClient({
url: import.meta.env.VITE_GRAPHQL_WS_URL ?? 'ws://localhost:4000/graphql',
connectionParams: () => ({
authorization: `Bearer ${localStorage.getItem('token')}`,
}),
})
)
// split: subscriptions → WS, rest → HTTP
const splitLink = split(
({ query }) => {
const def = getMainDefinition(query)
return def.kind === 'OperationDefinition' && def.operation === 'subscription'
},
wsLink,
from([errorLink, authLink, httpLink])
)
export const apolloClient = new ApolloClient({
link: splitLink,
cache: new InMemoryCache({
typePolicies: {
Product: {
keyFields: ['id'],
},
// pagination
Query: {
fields: {
products: {
keyArgs: ['categoryId', 'search'],
merge(existing, incoming, { args }) {
if (!args?.offset) return incoming
return {
...incoming,
items: [...(existing?.items ?? []), ...incoming.items],
}
},
},
},
},
},
}),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
errorPolicy: 'all',
},
query: {
fetchPolicy: 'network-only',
errorPolicy: 'all',
},
},
})
// main.tsx
import { ApolloProvider } from '@apollo/client'
import { apolloClient } from '@/lib/apollo/client'
function App() {
return (
<ApolloProvider client={apolloClient}>
<Router />
</ApolloProvider>
)
}
Code Generation of Types
npm install -D @graphql-codegen/cli @graphql-codegen/client-preset
npx graphql-codegen init
# codegen.ts or codegen.yml
import type { CodegenConfig } from '@graphql-codegen/cli'
const config: CodegenConfig = {
overwrite: true,
schema: 'http://localhost:4000/graphql',
documents: 'src/**/*.graphql',
generates: {
'src/gql/': {
preset: 'client',
config: {
useTypeImports: true,
strictScalars: true,
scalars: {
DateTime: 'string',
UUID: 'string',
},
},
},
},
}
export default config
npx graphql-codegen --watch # in dev
npx graphql-codegen # in CI
GraphQL Documents
# src/features/products/products.graphql
query GetProducts($categoryId: ID!, $search: String, $offset: Int, $limit: Int) {
products(categoryId: $categoryId, search: $search, offset: $offset, limit: $limit) {
items {
id
name
price
stock
category {
id
name
}
}
total
hasMore
}
}
query GetProduct($id: ID!) {
product(id: $id) {
id
name
description
price
stock
images { url alt }
category { id name }
}
}
mutation CreateProduct($input: CreateProductInput!) {
createProduct(input: $input) {
id
name
price
}
}
mutation UpdateProduct($id: ID!, $input: UpdateProductInput!) {
updateProduct(id: $id, input: $input) {
id
name
price
stock
}
}
subscription OnStockUpdate($productId: ID!) {
stockUpdated(productId: $productId) {
productId
stock
}
}
Hooks After Code Generation
// features/products/useProducts.ts
import { useQuery, useMutation, useSubscription } from '@apollo/client'
import {
GetProductsDocument,
GetProductDocument,
CreateProductDocument,
UpdateProductDocument,
OnStockUpdateDocument,
} from '@/gql/graphql'
export function useProducts(categoryId: string) {
return useQuery(GetProductsDocument, {
variables: { categoryId },
notifyOnNetworkStatusChange: true,
})
}
export function useProduct(id: string) {
return useQuery(GetProductDocument, {
variables: { id },
skip: !id,
})
}
export function useCreateProduct() {
return useMutation(CreateProductDocument, {
update(cache, { data }) {
if (!data?.createProduct) return
cache.modify({
fields: {
products(existing) {
return {
...existing,
items: [data.createProduct, ...existing.items],
total: existing.total + 1,
}
},
},
})
},
})
}
export function useStockSubscription(productId: string) {
return useSubscription(OnStockUpdateDocument, {
variables: { productId },
onData: ({ client, data }) => {
const update = data.data?.stockUpdated
if (!update) return
client.cache.modify({
id: client.cache.identify({ __typename: 'Product', id: update.productId }),
fields: {
stock: () => update.stock,
},
})
},
})
}
Usage in Component
function ProductList({ categoryId }: { categoryId: string }) {
const { data, loading, error, fetchMore, networkStatus } = useProducts(categoryId)
const [createProduct, { loading: creating }] = useCreateProduct()
if (loading && networkStatus !== NetworkStatus.fetchMore) return <Skeleton />
if (error) return <ErrorMessage error={error} />
const products = data?.products.items ?? []
const hasMore = data?.products.hasMore ?? false
return (
<>
<ProductGrid products={products} />
{hasMore && (
<button
onClick={() => fetchMore({ variables: { offset: products.length } })}
disabled={networkStatus === NetworkStatus.fetchMore}
>
Load More
</button>
)}
</>
)
}
Fragments for Reuse
# fragments.graphql
fragment ProductBasic on Product {
id
name
price
stock
}
fragment ProductFull on Product {
...ProductBasic
description
images { url alt }
category { id name }
}
import { useFragment } from '@apollo/client'
import { ProductBasicFragmentDoc } from '@/gql/graphql'
// read fragment from cache without separate request
function ProductCardFromCache({ id }: { id: string }) {
const { data } = useFragment({
fragment: ProductBasicFragmentDoc,
from: { __typename: 'Product', id },
})
return <div>{data?.name}</div>
}
Apollo DevTools
Browser extension for Chrome/Firefox — visualizes cache, shows all executed queries, mutations and subscriptions, provides explorer for arbitrary GraphQL queries directly from the browser.
What We Do
Set up Apollo Client with HTTP + WS link, authentication and error handling, configure code generation of types, write queries/mutations/subscriptions in .graphql files, set up cache policies for paginated collections, cover with tests using MockedProvider.
Timeline: 3–5 days, including code generation setup and writing hooks for all entities.







