← Back to list

Caching Safely with Composite Keys in Apollo GraphQL

How to use custom caching in Apollo GraphQL alongside custom ESLint rules to lockdown new points of failure.

Ejaustinforbes · 2026-06-25 10:09 · 0 claps · 2.5 min read
#graphql #apollo #apollo-client #eslint
Open on Medium ↗

Caching Safely with Composite Keys in Apollo GraphQL

How to use custom caching in Apollo GraphQL alongside custom ESLint rules to lockdown new points of failure.

Recently a member of my team ran into a problem — Apollo GraphQL’s cache works fantastically in our project, but without a unique identifier it cannot/will-not insert new entries.

In our case we have a realtime workflow which requires regular access to the cache for a variety of types. All was well until we found an unindexable type in our schema:

type Port {
  transportType: TransportType!
  transportCode: String!
}

Our cache is configured to use the id property to define store objects.

To get around this issue, the engineer used a pretty slick approach show in the documentation:

const cache = InMemoryCache({
  typePolicies: {
    Port: {
      keyFields: ['code', 'transportType'],
    }
  }
}

In Apollo parlance, a keyField is the identifying field(s) for this type. So in our case, it worked swimmingly. We know that two of our Ports may share the same code but not the same transportType, and vice-versa. Specifying an array of multiple keyFields tells Apollo to form identify these objects by the composition of these two fields! It works great.

But there is a problem, GraphQL documents specify their requirements field-by-field. Moving to a composite key introduces a new hidden dependency on those fields. If Apollo can’t find them, it can’t abide by the type policy and might throw a nasty error in production.

Luckily I installed a pretty nice package in this project some time ago, GraphQL-ESLint, a plugin for the widely used ESLint which brings its awesome powers over to your GraphQL documents.

This package has a wide variety of rule sets with some pretty impressive capabilities, it’s pretty much a must have if you are using GraphQL in my opinion.

Despite this, while it has a built-in rule for making selections required, our situation requires selections on one type in particular.

Luckily since at least ESLint 9 custom rules are well supported, let’s write one:

// eslint-local-rules.cjs
const { requireGraphQLSchema } = require('@graphql-eslint/eslint-plugin')

module.exports = {
  rules: {
    'require-port-composite-key': {
      meta: {
        type: 'problem',
        messages: {
          missingCompositeFields:
            'Custom Error: You must select BOTH the `code` and `transportType` fields on Port! They form the composite key required for cache normalization.',
        },
      },
      create(context) {
        requireGraphQLSchema('require-port-composite-key', context)

        return {
          SelectionSet(node) {
            const typeInfo = node.typeInfo()
            const typeName = typeInfo?.gqlType?.toString().replace(/[![\]]/g, '') // Strips out ! and [] wrappers

            if (typeName === 'Port') {
              const selectedFields = node.selections
                .filter((sel) => sel.kind === 'Field')
                .map((sel) => sel.name.value)

              const hasCode = selectedFields.includes('code')
              const hasTransportType = selectedFields.includes('transportType')

              if (!hasCode || !hasTransportType) {
                context.report({
                  node,
                  messageId: 'missingCompositeFields',
                })
              }
            }
          },
        }
      },
    },
  },
}

Nice! Our language parser will now perform a callback on each node it comes across. We’ll check manually if it’s our target type, Port, and if so we will read the AST directly in the ESLint run-time for our rule!

Now we need to plug it into our ESLint-GraphQL config:

// eslint.config.cjs

const graphqlPlugin = require('@graphql-eslint/eslint-plugin')
const localRules = require('./eslint-local-rules.cjs')

const rules = {
  files: ['**/*.graphql'],
  plugins: {
    '@graphql-eslint': graphqlPlugin,
    'local-rules': localRules,
  },
  languageOptions: {
    parser: graphqlPlugin.parser,
  },
  rules: {
    ...graphqlPlugin.configs['operations-recommended'].rules,
    'local-rules/require-port-composite-key': "error",
  }
}

To get a nice and bright error message on our lints:

fragment Voyage on Voyage {
  id
  no
  vesselName
  vesselCode
  departurePort {
    code
  }
}
.../fragment.voyage.graphql
  6:17  error  Custom Error: You must select BOTH the `code` and `transportType` fields on Port! They form the composite key required for cache normalization  local-rules/require-port-composite-key

✖ 1 problem (1 error, 0 warnings)

If you want to read more about custom caching in Apollo, check out this link. Whatever you come up with, I’m confident that with custom linters your whole team will be able to write GraphQL that works with it!


메타데이터
post_id
6ad146933cef
slug
caching-safely-with-composite-keys-in-apollo-graphql-6ad146933cef
url
https://medium.com/@ejaustinforbes/caching-safely-with-composite-keys-in-apollo-graphql-6ad146933cef
canonical_url
https://medium.com/@ejaustinforbes/caching-safely-with-composite-keys-in-apollo-graphql-6ad146933cef
author_url
https://medium.com/@ejaustinforbes
status
ok
fetched_at
2026-07-09 18:09:57