Apollo Federation v2 Router Configuration vs Schema Stitching
This guide sits in Advanced GraphQL Federation Patterns and puts two ways of building one graph over a headless CMS and its neighbours side by side: a Federation v2 supergraph served by the Apollo Router, and a schema stitching gateway built with GraphQL Tools. Both give frontends one endpoint. They differ in who declares relationships, where query planning happens, what vendors must support, and how the setup evolves as teams grow.
The question comes up in almost every headless architecture review, usually phrased as “should we federate?”. The honest answer depends less on technology than on ownership. Federation assumes that each part of the graph has an owning team that can add keys and reference resolvers to its service. Stitching assumes nothing of the services and puts all integration knowledge in one gateway. Seeing the same integration built both ways makes the trade-off concrete.
The Problem
A retailer wants product pages that combine prices and stock from its commerce platform with descriptions, imagery and SEO fields from a headless CMS. The frontend team currently makes two requests per page and merges the results in React, with duplicated types, inconsistent loading states and no shared caching. The platform team proposes a unified graph, and the discussion immediately splits: the commerce team owns its GraphQL service and is happy to add federation keys, but the CMS is a SaaS product whose GraphQL API cannot be modified. One camp argues for federation with a wrapper subgraph around the CMS; the other argues for stitching both APIs as they are.
Both can work. What decides it is who will maintain the relationships between product and editorial data, how many more sources will join, and whether the organization wants that knowledge in one gateway or spread across services.
How Each Approach Declares the Product Join
In Federation v2, each subgraph declares the entities it contributes to with @key. The commerce subgraph owns Product with id, price and stock. The editorial subgraph, a thin service wrapping the CMS, contributes description and seoTitle to the same Product entity and implements __resolveReference to fetch them by id. Composition in CI produces a supergraph schema, and the router plans each query: fetch products from commerce, then fetch the editorial fields for those ids from the editorial subgraph in one batched _entities call.
A stitching gateway with merged types produces a very similar pattern of requests, but the plan comes from its merge configuration rather than from directives in the services, which is the heart of the difference.
In stitching, neither API knows about the other. The gateway introspects both, prefixes their types to avoid collisions, and declares the relationship itself: CmsProduct gains a commerce field resolved by delegating to the commerce API’s productBySku query, or a merged type configuration tells the gateway how to fetch each type’s fields from each service. The gateway plans delegation per field, according to the rules written in its configuration.
Implementation: The Federation v2 Setup
The federated version needs three artifacts: subgraph schemas with keys, a composition config and the router configuration. The editorial subgraph wraps the CMS, as shown in the federation topic.
# supergraph.yaml: composed in CI with `rover supergraph compose --config supergraph.yaml > supergraph.graphql`
federation_version: =2.5.0
subgraphs:
commerce:
routing_url: https://commerce.internal/graphql
schema:
file: ./subgraphs/commerce.graphql
editorial:
routing_url: https://editorial.internal/graphql
schema:
file: ./subgraphs/editorial.graphql
# router.yaml: run with `router --config router.yaml --supergraph supergraph.graphql`
supergraph:
listen: 0.0.0.0:4000
introspection: false
headers:
all:
request:
- propagate:
named: x-locale
traffic_shaping:
subgraphs:
editorial:
timeout: 800ms
commerce:
timeout: 1500ms
limits:
max_depth: 8
include_subgraph_errors:
all: false
The commerce subgraph declares type Product @key(fields: "id") { id: ID! price: Money! stock: Int! }, and the editorial subgraph declares type Product @key(fields: "id") { id: ID! description: String seoTitle: String }. Composition merges them into one Product with all five fields, and fails in CI if the two disagree about key types or field ownership.
Implementation: The Stitching Setup
The stitched version needs one gateway service. Merged types tell the gateway how to fetch a Product from each service by key, which gives similar batching behaviour to federation without changing either API.
// stitching-gateway.ts
import { stitchSchemas } from "@graphql-tools/stitch";
import { schemaFromExecutor } from "@graphql-tools/wrap";
import { buildHTTPExecutor } from "@graphql-tools/executor-http";
import { RenameTypes } from "@graphql-tools/wrap";
import { createYoga } from "graphql-yoga";
import { createServer } from "node:http";
const commerceExec = buildHTTPExecutor({ endpoint: "https://commerce.internal/graphql" });
const cmsExec = buildHTTPExecutor({
endpoint: process.env.CMS_GRAPHQL_URL ?? "",
headers: { Authorization: `Bearer ${process.env.CMS_DELIVERY_TOKEN ?? ""}` },
});
const gatewaySchema = stitchSchemas({
subschemas: [
{
schema: await schemaFromExecutor(commerceExec),
executor: commerceExec,
merge: {
Product: { selectionSet: "{ id }", fieldName: "productsByIds", key: ({ id }: { id: string }) => id, argsFromKeys: (ids: string[]) => ({ ids }) },
},
},
{
schema: await schemaFromExecutor(cmsExec),
executor: cmsExec,
transforms: [new RenameTypes((name) => (name === "ProductEditorial" ? "Product" : `Cms${name}`))],
merge: {
Product: { selectionSet: "{ id }", fieldName: "productEditorialByIds", key: ({ id }: { id: string }) => id, argsFromKeys: (ids: string[]) => ({ ids }) },
},
},
],
});
createServer(createYoga({ schema: gatewaySchema })).listen(4000);
This assumes the CMS exposes a query that fetches editorial entries by a list of product ids; where it does not, a small custom resolver in the gateway can call the CMS’s REST API instead. The key point is that the relationship lives entirely in this file, which one team owns.
Configuration Reference
| Concern | Federation v2 | Stitching |
|---|---|---|
| Join definition | @key in each subgraph schema |
merge config in the gateway |
| Composition | rover supergraph compose in CI |
schema built at gateway startup or in CI |
| Header forwarding | router headers rules |
executor options per subschema |
| Timeouts | router traffic_shaping per subgraph |
executor or fetch timeouts per subschema |
| Limits and safelisting | router configuration | custom plugins in the gateway server |
| Tracing | router telemetry | server plugins and executor instrumentation |
Gotchas & Edge Cases
- Wrapping a SaaS CMS is still work. Federation with a CMS usually means writing and operating an editorial subgraph. If only one team would maintain it, stitching the CMS directly may cost less.
- Type renaming in stitching. Renaming the CMS’s
ProductEditorialtoProductmakes merging possible but hides the CMS’s own naming from developers. Document the mapping in the gateway repository. - Runtime introspection. Building the stitched schema by introspecting services at startup makes the gateway fail if a service is down during a deploy. Snapshot schemas in CI for production.
- Mixed setups. A stitched gateway can itself be one subgraph of a federated supergraph. That is a valid transition, but keep it deliberate, because debugging spans two planners.
Worked Example
The retailer from the problem statement built both prototypes in a week. The federated version needed an editorial subgraph of about 150 lines and gave the commerce team ownership of its part of the join, with composition checks in its own pipeline. The stitched version was a single file and worked against the CMS as-is, but every future source would add more configuration to that one file. Because two more teams planned to expose GraphQL services within the year, the retailer chose federation for the owned services and wrapped the CMS in an editorial subgraph; a stitched subgraph was kept only for a third-party reviews API that could not be changed.
Rollout Checklist
- List every source, its owner and whether its API can be changed.
- Federate services you own; stitch or wrap APIs you cannot change.
- Compose or build the schema in CI and deploy the artifact, never compose at runtime in production.
- Configure headers, timeouts and limits explicitly in whichever runtime you choose.
- Revisit the decision as sources and teams grow, using a stitched subgraph as a bridge when needed.
Frequently Asked Questions
Is schema stitching deprecated?
No. It is maintained in the GraphQL Tools ecosystem and remains the practical way to merge APIs you cannot modify. Federation is better supported for multi-team setups with services you own.
Can the Apollo Router serve a stitched schema?
Not directly; the router serves federated supergraphs. A stitched gateway can be added as a subgraph if it implements the federation specification, for example through a subgraph wrapper.
Which is faster?
Both batch well when configured properly. The router’s compiled query planner usually has lower overhead per request, while a well-configured stitching gateway with merged types performs comparably for a small number of sources.
Can we start with stitching and move to federation later?
Yes, and it is a common path. Clients see the same schema either way, so replacing the gateway with a router is invisible to them as long as type and field names stay stable.
Which is easier to hand over to a new team?
Federation, because each subgraph’s schema documents its own part of the graph and composition errors point to the responsible service. A stitching gateway concentrates knowledge in one codebase, which is simpler at first and harder to share later.