Skip to content
All essays
CraftMarch 30, 202412 min

GraphQL Caching Strategies

Implement effective caching for GraphQL APIs

Ü
Ümit Uz
Mobile & Full Stack Developer

Caching is crucial for GraphQL performance. Unlike REST with multiple endpoints, GraphQL has a single endpoint that requires sophisticated caching strategies.

Understanding GraphQL Caching

Cache Identifiers

Apollo Client uses __typename and id fields:

graphql
query GetUser($id: ID!) {
  user(id: $id) {
    id
    __typename
    name
    email
  }
}

Cache Storage

javascript
import { ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
  uri: '/graphql',
  cache: new InMemoryCache({
    typePolicies: {
      User: {
        keyFields: ['id']
      },
      Product: {
        keyFields: ['sku']
      }
    }
  })
});

Cache Invalidation

Manual Invalidation

javascript
import { useMutation, useQuery } from '@apollo/client';

const UPDATE_USER = gql`
  mutation UpdateUser($id: ID!, $name: String!) {
    updateUser(id: $id, name: $name) {
      id
      name
    }
  }
`;

function UserProfile({ userId }) {
  const [updateUser] = useMutation(UPDATE_USER);

  const handleUpdate = async (name) => {
    await updateUser({
      variables: { id: userId, name },
      update(cache, { data: { updateUser } }) {
        cache.modify({
          id: cache.identify({ __typename: 'User', id: userId }),
          fields: {
            name() {
              return updateUser.name;
            }
          }
        });
      }
    });
  };
}

Refetch Queries

javascript
const [addComment] = useMutation(ADD_COMMENT, {
  refetchQueries: [
    { query: GET_POST_COMMENTS, variables: { postId } }
  ]
});

Optimistic UI

javascript
const [addComment] = useMutation(ADD_COMMENT, {
  optimisticResponse: {
    addComment: {
      __typename: 'Comment',
      id: 'temp-id',
      content: newComment,
      author: currentUser
    }
  },
  update(cache, { data: { addComment } }) {
    cache.modify({
      id: cache.identify({ __typename: 'Post', id: postId }),
      fields: {
        comments(existing = []) {
          return [...existing, addComment];
        }
      }
    });
  }
});

Advanced Caching

Cache Redirects

javascript
const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        user: {
          read(_, { args, toReference }) {
            return toReference({
              __typename: 'User',
              id: args.id
            });
          }
        }
      }
    }
  }
});

Field Policy Caching

javascript
const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        feed: {
          keyArgs: ['type'],
          merge(existing = [], incoming) {
            return [...existing, ...incoming];
          }
        }
      }
    }
  }
});

Pagination Caching

javascript
const { data, fetchMore } = useQuery(GET_MESSAGES, {
  variables: { limit: 10, cursor: null },
  notifyOnNetworkStatusChange: true
});

const loadMore = () => {
  fetchMore({
    variables: {
      cursor: data.messages[data.messages.length - 1].cursor
    },
    updateQuery(previous, { fetchMoreResult }) {
      return {
        messages: [
          ...previous.messages,
          ...fetchMoreResult.messages
        ]
      };
    }
  });
};

Server-Side Caching

Response Caching

javascript
import { responseCachePlugin } from '@apollo/server-plugin-response-cache';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [
    responseCachePlugin({
      session: (ctx) => ctx.request.http.headers.get('session-id'),
      cache: new RedisCache({
        host: 'redis-server'
      })
    })
  ]
});

DataLoader Pattern

javascript
import DataLoader from 'dataloader';

const userLoader = new DataLoader(async (ids) => {
  const users = await db.users.findMany({
    where: { id: { in: ids } }
  });

  return ids.map(id =>
    users.find(user => user.id === id)
  );
});

const resolvers = {
  User: {
    posts: (user) => {
      return postLoader.loadMany(user.postIds);
    }
  }
};

Persisted Queries

javascript
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash/hashing';
import { ApolloLink, HttpLink } from '@apollo/client';

const httpLink = new HttpLink({ uri: '/graphql' });

const persistedLink = createPersistedQueryLink({
  sha256,
  useGETForHashedQueries: true
});

const client = new ApolloClient({
  link: persistedLink.concat(httpLink),
  cache: new InMemoryCache()
});

Cache Strategies

Cache-First

javascript
const { data } = useQuery(GET_USER, {
  fetchPolicy: 'cache-first'
});

Network-Only

javascript
const { data } = useQuery(GET_USER, {
  fetchPolicy: 'network-only'
});

Cache-and-Network

javascript
const { data } = useQuery(GET_USER, {
  fetchPolicy: 'cache-and-network'
});

Standby Queries

javascript
const { data } = useQuery(GET_DASHBOARD, {
  fetchPolicy: 'cache-first',
  nextFetchPolicy: 'cache-only',
  pollInterval: 30000
});

Cache Reset

Complete Reset

javascript
import { useApolloClient } from '@apollo/client';

function LogoutButton() {
  const client = useApolloClient();

  const logout = () => {
    client.resetStore();
    // or
    client.clearStore();
  };
}

Selective Eviction

javascript
client.evict({ id: 'User:123' });
client.evict({ fieldName: 'feed' });
client.gc();

Monitoring Cache

Cache Performance

javascript
const client = new ApolloClient({
  uri: '/graphql',
  cache: new InMemoryCache(),
  defaultOptions: {
    watchQuery: {
      fetchPolicy: 'cache-first',
      errorPolicy: 'all',
      notifyOnNetworkStatusChange: true
    }
  }
});

// Monitor cache hits
client.onCacheStoreWrite(() => {
  console.log('Cache updated');
});

Cache Inspection

javascript
import { useApolloClient } from '@apollo/client';

function CacheInspector() {
  const client = useApolloClient();

  const inspect = () => {
    console.log(client.cache.extract());
  };

  return <button onClick={inspect}>Inspect Cache</button>;
}

Best Practices

  1. 1Always include __typename and id in queries
  2. 2Use optimistic responses for mutations
  3. 3Implement proper cache invalidation
  4. 4Use DataLoader for N+1 problems
  5. 5Set appropriate TTL for server-side caching
  6. 6Monitor cache hit rates
  7. 7Use cache-first for frequently accessed data
  8. 8Network-only for real-time data

Conclusion

Effective caching strategies dramatically improve GraphQL performance. Combine client and server-side caching for optimal results.

Enjoyed this essay? Share it.

Related essays

Next essay
Mastering Arrays and Linked Lists