Type-safe GraphQL resolvers for federated CMS

Federating multiple headless CMS instances behind one GraphQL gateway breaks the type inference that monolithic resolvers rely on. When schemas span distinct subgraphs, you get runtime null returns, silent field collisions, and circular dependency loops on cross-platform queries. Three controls keep it type-safe: explicit schema contracts, generated resolver types, and runtime validation at every subgraph boundary.

Explicit schema contracts

Federation stitches distributed graphs via entity definitions and @key directives. Each platform exposes a subgraph with isolated namespaces, and the gateway merges them in a supergraph composition step — but without strict contracts, fields collide at query time. Subgraph ownership is decided by your Headless CMS Architecture & Platform Selection, and that decision precedes the first resolver. Platform-native GraphQL layers need wrapping to expose federation-compatible types, and manual wrapping drifts from the supergraph fast.

The production standard decouples composition from execution: subgraphs publish schemas to a registry, and the router validates compatibility before deploy. Contract-first means content teams can iterate on editorial models without breaking frontend inference.

The three controls form a pipeline from compile-time contracts to runtime entity resolution:

flowchart TD
  Subgraphs["Subgraph schemas + @key"] --> Compose["Supergraph composition"]
  Compose --> Codegen["graphql-codegen typescript-resolvers"]
  Codegen --> Types["Generated resolver types + mappers"]
  Types --> Resolver["Resolver implementation"]
  Req["Federated query"] --> Resolver
  Resolver --> Zod{"Zod reference validation"}
  Zod -->|"valid"| Resolve["__resolveReference + cross-subgraph fetch"]
  Zod -.->|"invalid"| Err["Structured error to boundary"]
  Resolve --> Result["Type-safe response"]

Generated resolver types

Hand-written TypeScript interfaces for federated schemas don’t survive schema churn. Use @graphql-codegen/cli with typescript-resolvers to generate strict interfaces from the composed supergraph, so resolver signatures match the gateway contract exactly. This codegen.ts isolates domain models from GraphQL types via mappers and enforces strict context typing:

TypeScript
import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
  schema: 'http://localhost:4000/supergraph',
  documents: ['src/**/*.graphql'],
  generates: {
    './src/generated/resolvers.ts': {
      plugins: ['typescript', 'typescript-resolvers'],
      config: {
        strictScalars: true,
        contextType: '../context#FederationContext',
        mappers: {
          Article: '../types#ArticleEntity',
          Author: '../types#AuthorEntity',
          MediaAsset: '../types#MediaEntity'
        },
        useIndexSignature: true
      }
    }
  }
};

export default config;

The mappers config keeps CMS-specific metadata — Contentful sys fields, Sanity _type strings — out of the client by mapping GraphQL types to internal domain models, separating transport contracts from persistence. See the GraphQL Code Generator documentation for plugin and CI details.

Runtime validation and entity resolution

Generated types only cover compile time. At runtime, federated resolvers implement __resolveReference for entity stitching. When a query spans Contentful_Article and Sanity_Author, the gateway delegates across network boundaries, and the resolver chain runs sequentially unless you parallelize with Promise.all or DataLoader.

A missing __resolveReference returns a silent null at the gateway, and reference keys must match the @key directive exactly. This resolver shows strict type alignment, Zod payload validation, and cross-subgraph delegation:

TypeScript
import { Resolvers } from '../generated/resolvers';
import { z } from 'zod';

const ArticleRefSchema = z.object({
  id: z.string().uuid(),
  __typename: z.literal('Article')
});

export const resolvers: Resolvers = {
  Article: {
    __resolveReference: async (ref, { dataSources }) => {
      const validated = ArticleRefSchema.parse(ref);
      const article = await dataSources.contentful.getArticleById(validated.id);
      return { ...validated, ...article };
    },
    author: async (parent, _, { dataSources }) => {
      if (!parent.authorId) return null;
      // Cross-subgraph delegation to Sanity author graph
      return dataSources.sanity.getAuthorById(parent.authorId);
    },
    relatedContent: async (parent, _, { dataSources }) => {
      // Parallelized fetch for performance optimization
      const [relatedArticles, relatedMedia] = await Promise.all([
        dataSources.contentful.getRelatedArticles(parent.id),
        dataSources.assetStore.getMediaByArticle(parent.id)
      ]);
      return { articles: relatedArticles, media: relatedMedia };
    }
  }
};

Zod validation is a defensive boundary before any external data source is called: it stops undefined propagation and emits structured errors that frontend error boundaries can surface. For routing and entity-resolution mechanics, see the Apollo Federation 2 documentation.

DX and multi-tenant safeguards

Schema drift hits DX directly — broken builds, delayed deploys, QA overhead. Run codegen in a pre-commit hook or CI so resolver signatures are validated before merge.

Multi-tenant environments need more: context injection should carry tenant IDs, cache-control headers, and rate-limit tokens, and resolvers must stay stateless and idempotent to scale horizontally. Use DataLoader for batched entity resolution to avoid N+1 when stitching across platforms. Per Content Modeling Best Practices, shared entities like Author, Category, and Media should be owned by a single authoritative subgraph and referenced by foreign key elsewhere — that cuts composition conflicts and simplifies cache invalidation.

Conclusion

Type safety in a federated CMS comes from three layers working together: supergraph composition, generated resolver types, and Zod validation at runtime. Together they eliminate the silent null returns and field collisions that otherwise surface only in production.