Schema Stitching for Multi-Vendor Headless Architectures

Schema stitching unifies disparate vendor GraphQL endpoints — CMS, commerce, DAM, search — into one frontend-facing schema without asking any vendor to adopt a shared SDK. The gateway introspects each endpoint, normalizes conflicting types, and resolves cross-service references at runtime. It’s the integration path when vendors export static schemas but block the runtime federation hooks that Advanced GraphQL Federation Patterns require.

The distinction matters: federation relies on explicit @key directives, subgraph ownership, and vendor-side federation SDKs. Stitching operates purely at the introspection level. That makes it the option for legacy CMS instances, third-party SaaS, and governed enterprise endpoints that prohibit federation hooks.

Stitching versus federationSchema stitching and federation compared on vendor requirements, type ownership, query planning and when each is the better choice.PropertySchema stitchingFederationVendor changes needednonesubgraph support or wrapperType ownershipgateway transformsdeclared by subgraphsQuery planningdelegation per fieldrouter plans across subgraphsScale sweet spota few vendorsmany teams, many subgraphsBest forthird-party SaaS APIsservices you own
Stitching asks nothing of vendors; federation asks for keys and subgraph support in return for better planning and ownership.

The gateway abstraction

The stitching gateway is a transparent proxy: it aggregates multiple schemas into one executable surface, intercepts client queries, delegates field resolution to the right downstream service, and merges results. No vendor API changes. This fits Headless CMS Architecture & Platform Selection decisions that prioritize composability over monolithic coupling.

The catch is normalization. Vendors expose conflicting type names, divergent scalars, and inconsistent pagination (offset vs. cursor). The gateway must apply deterministic transforms during schema construction or hit runtime collisions and unpredictable client typing.

The gateway introspects each vendor, normalizes types, and delegates cross-service fields at query time:

Stitching gateway construction and delegationAt startup the gateway introspects each vendor, renames types and filters root fields, and merges the schemas; at query time it delegates fields to the CMS, commerce and DAM vendors using declared selection sets and merges the results for the client.Frontend clientStitching gatewayIntrospect +RenameTypes, FilterCMS vendorCommerce vendorDAM vendormerged schemadelegatedelegateToSchemadelegateToSchema
Vendors need no changes; all normalization lives in the gateway's transforms and delegation rules.

Introspection and type normalization

The flow starts with runtime introspection against each vendor. Disable introspection caching in development to catch schema drift; in production, cache snapshots to cut startup latency and avoid vendor rate limits during initialization.

TypeScript
import { stitchSchemas } from '@graphql-tools/stitch';
import { introspectSchema, RenameTypes, FilterRootFields } from '@graphql-tools/wrap';
import { delegateToSchema } from '@graphql-tools/delegate';
import { GraphQLSchema, OperationTypeNode, printSchema } from 'graphql';
import type { SubschemaConfig } from '@graphql-tools/delegate';

interface VendorConfig {
  uri: string;
  headers: Record<string, string>;
  prefix: string;
  excludeRootFields?: string[];
}

async function buildStitchedGateway(vendors: VendorConfig[]): Promise<GraphQLSchema> {
  const subschemas: SubschemaConfig[] = [];

  for (const vendor of vendors) {
    const schema = await introspectSchema({
      uri: vendor.uri,
      headers: vendor.headers,
      // Production optimization: cache introspection results
      // to avoid repeated vendor rate-limit exhaustion
    });

    subschemas.push({
      schema,
      transforms: [
        new RenameTypes((name) => `${vendor.prefix}${name}`),
        ...(vendor.excludeRootFields
          ? [new FilterRootFields((op, field) => !vendor.excludeRootFields!.includes(field))]
          : []),
      ],
    });
  }

  // Extend types to establish cross-service relationships
  const mergedSchema = stitchSchemas({
    subschemas,
    typeDefs: `
      extend type CmsProduct {
        commerceInventory: CommerceInventory
        damAssets: [DamAsset!]
      }
    `,
    resolvers: {
      CmsProduct: {
        commerceInventory: {
          selectionSet: `{ sku }`,
          resolve: async (parent, _args, context, info) => {
            return delegateToSchema({
              schema: subschemas[1], // Commerce subschema config
              operation: OperationTypeNode.QUERY,
              fieldName: 'inventoryBySku',
              args: { sku: parent.sku },
              context,
              info,
            });
          },
        },
      },
    },
  });

  // Validate merged schema before exposing to clients
  const schemaString = printSchema(mergedSchema);
  if (schemaString.includes('TypeConflictError') || schemaString.includes('undefined')) {
    throw new Error('Schema merge validation failed. Inspect printSchema output.');
  }

  return mergedSchema;
}

Three production requirements show up above:

  1. Deterministic prefixing: RenameTypes prevents collisions when vendors share names like Page, User, Asset.
  2. Root filtering: FilterRootFields strips vendor-specific mutations or queries that shouldn’t reach the unified client.
  3. Explicit delegation: selectionSet declares the exact payload the upstream needs, so downstream resolvers get only required context.

Type collisions and drift

Most stitching failures surface during type resolution. When two vendors define overlapping Page, Asset, or User types without transforms, schema construction throws TypeConflictError. Worse are silent overrides: two vendors with identical field names but mismatched scalar types produce unpredictable client behavior.

Guard against drift with CI schema validation — diff introspection snapshots against a baseline using graphql-diff or similar. When a vendor updates an API without versioning, the gateway must reject the incompatible merge or apply a fallback transform. The GraphQL specification requires strict type compatibility during merging; violating it produces runtime failures that slip past standard error boundaries.

Delegation latency

Misaligned selectionSet config drives latency. Over-fetching trips vendor rate limits and bloats payloads; under-fetching propagates null when a required field is missing from the parent. To tune delegation:

Latency of a product page query by delegation setupp95 latency for a product page query that stitches CMS, commerce and DAM data, with sequential delegation, batched delegation and batched delegation plus gateway caching of inventory lookups.Sequential delegation980 msBatched delegation410 msBatched + cached inventory260 ms
Batching removes per-item round trips; caching removes repeat lookups within their TTL.
  • Precise selection sets: request only the fields downstream resolution needs.
  • Batched delegation: enable batching on delegateToSchema when resolving multiple parents to cut round trips.
  • Response caching: add DataLoader or a Redis cache at the gateway for high-frequency queries like inventoryBySku or assetByHash.
  • Timeout enforcement: wrap delegation in AbortController so a degraded vendor doesn’t cascade.

Instrument the gateway with OpenTelemetry spans to trace cross-service resolution and catch bottlenecks in TTFB, resolver execution time, and cache hit ratio before they reach the frontend.

Governance and header propagation

Multi-vendor stitching complicates authorization. Never hardcode vendor tokens. Extract auth headers from the incoming client request, validate scopes, and forward only the credentials each subschema needs.

TypeScript
const context = async ({ req }) => {
  const cmsToken = extractScopedToken(req.headers.authorization, 'cms:read');
  const commerceKey = extractScopedToken(req.headers.authorization, 'commerce:inventory');

  return {
    cmsHeaders: { Authorization: `Bearer ${cmsToken}` },
    commerceHeaders: { 'X-Vendor-Auth': commerceKey },
    // Propagate tenant ID for multi-tenant routing
    tenantId: req.headers['x-tenant-id'],
  };
};

Compliance requires audit trails for cross-service access. Log delegation paths, response sizes, and error classes at the gateway without exposing PII or tokens. Rotate vendor credentials and enforce least-privilege scopes.

When to stitch instead of federate

Stitching wins when vendor APIs lack federation support, enforce strict schema-export policies, or span organizational boundaries with limited API governance. It gives fast integration and minimal lock-in. But past roughly five stitched services, delegation complexity and latency usually justify migrating to federation with explicit subgraph ownership. Treat stitching as transitional or permanent based on vendor maturity, compliance needs, and DX targets — done right, it delivers one GraphQL surface without forcing vendor cooperation.

Configuration Reference

Setting Value Why
Type prefix per vendor Cms, Commerce, Dam Prevents collisions on common names.
Root field filter only fields clients need Keeps vendor admin mutations out of the public graph.
Introspection snapshot cached, refreshed in CI Fast startup and drift detection.
Delegation batching enabled Fewer round trips for lists.
Vendor timeout per vendor, via AbortController One slow vendor cannot stall the page.

Gotchas & Edge Cases

  • String checks for validation. The example scans printSchema output for TypeConflictError or undefined, which catches nothing reliably, because conflicts throw during stitching rather than appearing in the printed schema. Validate by building the schema in CI and running a set of recorded client operations against it.
  • Subschema by index. subschemas[1] couples delegation to array order; adding a vendor silently changes the target. Keep named references to each subschema config.
  • Introspection disabled by vendors. Some production APIs disable introspection. Load the vendor’s published SDL file instead, and update it deliberately when the vendor releases changes.
  • Scalar mismatches. Two vendors’ DateTime scalars may serialize differently. Normalize to one scalar in the gateway with a transform, or clients receive inconsistent formats.

Worked Example

A retailer combined a SaaS CMS, a commerce platform and a digital asset manager, none of which offered federation support. Stitching with type prefixes and three delegated cross-vendor fields gave frontend teams one schema in two weeks. Two years later, with seven vendors and noticeable delegation latency, they moved the two services they owned to federation subgraphs and kept stitching only for the SaaS APIs, wrapped in a thin stitched subgraph of its own.

Rollout Checklist

  • Snapshot each vendor’s schema and commit it; refresh and diff in CI.
  • Prefix types per vendor and filter root fields to what clients need.
  • Declare exact selection sets for every delegated field and batch delegation for lists.
  • Put timeouts and caching around each vendor, sized to its reliability and volatility.
  • Forward only scoped credentials per vendor, never the client’s token to every service.
  • Plan the exit: services you own should become federation subgraphs as they mature.

The same governance and code review discipline applies here as it does in federation: type-safe resolvers, generated from the stitched schema, keep delegation code honest when vendors change their APIs, as described in type-safe resolvers for federated CMS.

Frequently Asked Questions

Can stitching and federation be combined?

Yes. A stitched gateway can itself be exposed as one subgraph of a federated supergraph, which lets teams federate the services they own while stitching third-party APIs behind a single wrapper.

How do I handle vendor schema changes?

Refresh introspection snapshots in CI, diff them against the committed baseline, and fail the build on breaking changes, so a vendor update never reaches production unnoticed.

Is schema stitching still maintained?

Yes, through the GraphQL Tools project, and it remains the practical way to merge schemas you do not control.

How do I cache stitched responses?

Cache at two levels: vendor responses inside the gateway, keyed by the delegated query and variables with vendor-appropriate TTLs, and whole responses at the CDN when operations are public and persisted. Purge both from vendor webhooks where vendors offer them.

How do I test delegated fields?

Mock each vendor with recorded responses and assert that a query spanning vendors returns merged data with the expected shape. Include a test where one vendor times out, to confirm nullable delegated fields degrade gracefully.