GraphQL Federation Gateway Tutorial: Build a Unified Supergraph from Independent Services
Build a production-ready GraphQL federation gateway: compose subgraphs, resolve entities, add auth, performance, and deployment patterns.
Image used for representation purposes only.
Overview
GraphQL federation lets you compose multiple GraphQL services (subgraphs) into a single, unified API (the supergraph) that clients query through a gateway. This tutorial walks you end‑to‑end: designing federated subgraphs, wiring a gateway, adding entity resolution, propagating auth, and preparing for production.
You’ll build a simple marketplace composed of three subgraphs:
- Products: catalog data
- Users: profiles
- Reviews: user reviews that link users and products
We’ll use a Node.js gateway for clarity, and note an alternative with a high‑performance router.
Prerequisites
- Node.js 18+ and npm or Yarn
- Basic familiarity with GraphQL schemas and resolvers
- Three terminal windows (or a process manager) to run services concurrently
Folder layout (suggested):
federation-tutorial/
gateway/
subgraphs/
products/
users/
reviews/
Federation concepts in 90 seconds
- Subgraph: An independently owned GraphQL service with its own schema and resolvers.
- Entity: A type that can be referenced across subgraphs; identified by one or more key fields.
- Key: A directive that marks the unique identity of an entity (for example, @key(fields: “id”)).
- Reference resolver: Code that fetches an entity instance in the subgraph that owns it.
- Supergraph: The composed schema produced from all subgraphs.
- Gateway/Router: The single endpoint that plans, validates, and executes client queries across subgraphs.
Step 1 — Bootstrap three subgraphs
Each subgraph is just a GraphQL server. Below are minimal Node.js services using Apollo Server and federation helpers.
Create subgraphs/products/index.js:
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { buildSubgraphSchema } from '@apollo/subgraph';
import gql from 'graphql-tag';
// In-memory data for the demo
const products = [
{ id: 'p1', name: 'Noise-Cancelling Headphones', price: 199 },
{ id: 'p2', name: 'Ergonomic Keyboard', price: 129 },
];
const typeDefs = gql`
# Federation v2 directive imports are commonly handled automatically by tooling.
# Many setups still work without an explicit @link, but you can add it if your tooling requires.
type Product @key(fields: "id") {
id: ID!
name: String!
price: Int
}
type Query {
product(id: ID!): Product
topProducts(first: Int = 5): [Product!]!
}
`;
const resolvers = {
Query: {
product: (_, { id }) => products.find(p => p.id === id) || null,
topProducts: (_, { first }) => products.slice(0, first),
},
Product: {
// Required by federation to resolve references from other subgraphs
__resolveReference: (ref) => products.find(p => p.id === ref.id) || null,
},
};
const server = new ApolloServer({ schema: buildSubgraphSchema({ typeDefs, resolvers }) });
const { url } = await startStandaloneServer(server, { listen: { port: 4001 } });
console.log(`Products subgraph ready at ${url}`);
Create subgraphs/users/index.js:
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { buildSubgraphSchema } from '@apollo/subgraph';
import gql from 'graphql-tag';
const users = [
{ id: 'u1', username: 'ada' },
{ id: 'u2', username: 'grace' },
];
const typeDefs = gql`
type User @key(fields: "id") {
id: ID!
username: String!
}
type Query {
user(id: ID!): User
me: User
}
`;
const resolvers = {
Query: {
user: (_, { id }) => users.find(u => u.id === id) || null,
me: () => users[0],
},
User: {
__resolveReference: (ref) => users.find(u => u.id === ref.id) || null,
},
};
const server = new ApolloServer({ schema: buildSubgraphSchema({ typeDefs, resolvers }) });
const { url } = await startStandaloneServer(server, { listen: { port: 4002 } });
console.log(`Users subgraph ready at ${url}`);
Create subgraphs/reviews/index.js:
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { buildSubgraphSchema } from '@apollo/subgraph';
import gql from 'graphql-tag';
const reviews = [
{ id: 'r1', body: 'Great sound!', rating: 5, productId: 'p1', authorId: 'u1' },
{ id: 'r2', body: 'Comfortable typing', rating: 4, productId: 'p2', authorId: 'u2' },
];
const typeDefs = gql`
type Review {
id: ID!
body: String!
rating: Int!
author: User
product: Product
}
extend type User @key(fields: "id") {
id: ID! @external
reviews: [Review!]!
}
extend type Product @key(fields: "id") {
id: ID! @external
reviews: [Review!]!
}
type Query {
review(id: ID!): Review
}
`;
const resolvers = {
Query: {
review: (_, { id }) => reviews.find(r => r.id === id) || null,
},
Review: {
author: (r) => ({ __typename: 'User', id: r.authorId }),
product: (r) => ({ __typename: 'Product', id: r.productId }),
},
User: {
reviews: (user) => reviews.filter(r => r.authorId === user.id),
},
Product: {
reviews: (product) => reviews.filter(r => r.productId === product.id),
},
};
const server = new ApolloServer({ schema: buildSubgraphSchema({ typeDefs, resolvers }) });
const { url } = await startStandaloneServer(server, { listen: { port: 4003 } });
console.log(`Reviews subgraph ready at ${url}`);
Run each service in its own terminal:
node subgraphs/products/index.js
node subgraphs/users/index.js
node subgraphs/reviews/index.js
Step 2 — Understand entities and references
- Product and User are entities (@key on id) and “owned” by their defining subgraphs.
- The Reviews subgraph extends those types to add cross‑cutting fields (reviews) and returns references like
{ __typename: 'Product', id }so the gateway can delegate to the owning subgraph when necessary.
That’s the heart of federation: each domain owns its data, and other subgraphs can extend those types without tightly coupling code or databases.
Step 3 — Compose a supergraph and run a gateway
Create gateway/index.js:
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { ApolloGateway, IntrospectAndCompose, RemoteGraphQLDataSource } from '@apollo/gateway';
// Compose the supergraph by introspecting subgraphs in dev.
// In production, consider a precomposed supergraph SDL and a registry.
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'products', url: 'http://localhost:4001/' },
{ name: 'users', url: 'http://localhost:4002/' },
{ name: 'reviews', url: 'http://localhost:4003/' },
],
}),
buildService: ({ url }) => new RemoteGraphQLDataSource({
url,
// Will use this in a later step for auth/header propagation
willSendRequest({ request, context }) {
if (context?.authToken) request.http.headers.set('authorization', context.authToken);
if (context?.requestId) request.http.headers.set('x-request-id', context.requestId);
},
}),
});
const server = new ApolloServer({
gateway,
// Turn off Apollo Server’s default landing page in production
// introspection: process.env.NODE_ENV !== 'production',
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
context: async ({ req }) => ({
authToken: req.headers['authorization'] || '',
requestId: req.headers['x-request-id'] || `req-${Date.now()}`,
}),
});
console.log(`Gateway ready at ${url}`);
Start the gateway:
node gateway/index.js
Step 4 — Query the supergraph
Send a query to http://localhost:4000/ with your favorite client or curl:
query TopProductsWithReviews($first: Int!) {
topProducts(first: $first) {
id
name
price
reviews { id rating body author { id username } }
}
}
Example JSON response:
{
"data": {
"topProducts": [
{
"id": "p1",
"name": "Noise-Cancelling Headphones",
"price": 199,
"reviews": [
{ "id": "r1", "rating": 5, "body": "Great sound!", "author": { "id": "u1", "username": "ada" } }
]
},
{
"id": "p2",
"name": "Ergonomic Keyboard",
"price": 129,
"reviews": [
{ "id": "r2", "rating": 4, "body": "Comfortable typing", "author": { "id": "u2", "username": "grace" } }
]
}
]
}
}
The gateway planned one query that fan‑outs to the Products subgraph for the list, then to Reviews for each product’s reviews, and to Users for authors.
Step 5 — Add authentication and header propagation
Federation typically uses a front‑door auth policy at the gateway, with downstream context/header propagation to subgraphs.
- Validate tokens at the edge (gateway) and attach claims to context.
- Use buildService + willSendRequest (shown above) to forward only the headers or claims subgraphs need.
- In each subgraph, read headers from the request to enforce domain‑specific permissions.
Example (Products subgraph) to read forwarded headers:
import { startStandaloneServer } from '@apollo/server/standalone';
// ... existing server
await startStandaloneServer(server, {
listen: { port: 4001 },
context: async ({ req }) => ({
userId: req.headers['x-user-id'] || null,
// or parse claims from a JWT propagated by the gateway
}),
});
Tips:
- Keep PII and sensitive claims minimal; prefer opaque IDs.
- Consider a centralized auth service that the gateway consults during request lifecycle.
Step 6 — Performance patterns
- Dataloader per request: Batch and cache entity lookups in each subgraph to reduce N+1 issues.
- Bounded timeouts: Configure subgraph timeouts and fallbacks (e.g., default to partial data when reviews are slow).
- Cache where it counts:
- CDN cache for GET queries if your gateway supports APQ or GET.
- Subgraph‑level memoization for expensive lookups.
- Persisted/whitelisted operations: Enable APQ or a safelist to reduce planning and improve security.
Example Dataloader sketch inside Products:
import DataLoader from 'dataloader';
function makeProductLoader() {
return new DataLoader(async (ids) => {
// Replace with a single DB query: SELECT * FROM products WHERE id IN (...)
return ids.map(id => products.find(p => p.id === id) || null);
});
}
// in context
const loader = makeProductLoader();
// in resolvers use loader.load(id)
Step 7 — Local composition, checks, and CI/CD
- Local dev: Compose via gateway introspection (as above) for quick iteration.
- Precomposed supergraph: In delivery pipelines, compose subgraphs into a supergraph SDL artifact and deploy it with the gateway/router. This enables schema checks, reproducibility, and rollbacks.
- Contract tests: For each subgraph, add tests that validate entity resolvers and selection sets required by other subgraphs.
- Breaking change checks: Fail the build if changes remove fields used by clients or other subgraphs.
Step 8 — Observability and error handling
- Structured errors: Return domain‑specific codes from subgraphs; the gateway will surface partial data + errors[] by path.
- Tracing/metrics: Enable per‑subgraph timing and top resolvers; track error rates, P95 latency, and cold paths.
- Correlation: Pass a request ID from gateway to subgraphs and include it in logs.
Optional — Use a high‑performance router
A Rust‑based GraphQL router can replace the Node.js gateway for higher throughput. Typical flow:
- Compose a supergraph SDL as part of CI (from your subgraph SDLs).
- Start the router with that supergraph file and a YAML config (timeouts, CORS, header forwarding, caching, etc.).
Minimal router config example (conceptual):
# router.yaml
cors:
allow_any_origin: true
headers:
all:
request:
- propagate: ["authorization", "x-request-id"]
timeouts:
subgraph: 2s
# Start (example):
# router --supergraph ./supergraph.graphql --config ./router.yaml
Use this path when you need native performance, advanced retries, circuit‑breaking, or WASM plugins.
Troubleshooting checklist
- Composition fails: Ensure each entity has a @key and extension types mark external fields with @external.
- Failing reference resolvers: Implement __resolveReference in the owning subgraph and return null when not found.
- Infinite or slow queries: Add max depth/complexity at the gateway; consider persisted operations.
- Mixed versions: Align your federation library versions across subgraphs and gateway/router.
- CORS: If calling the gateway from browsers, configure CORS at the gateway (and disable it on internal subgraphs).
Hardening for production
- Run the gateway/router close to subgraphs to minimize tail latency.
- Separate read vs. write paths if needed (writes might bypass heavy caching).
- Rate limiting and abuse prevention at the edge.
- Blue/green deploys for subgraphs and the gateway; validate with synthetic checks.
Recap
You created three federated subgraphs, composed them into a supergraph, and exposed a single endpoint through a gateway. You learned how entities, keys, and reference resolvers let teams own their domains while delivering a unified API. From here, add real data sources, enforce auth, and invest in composition checks to safely evolve your supergraph at scale.
Related Posts
Build a Real-Time Dashboard with GraphQL Subscriptions: A Step-by-Step Guide
Build a real-time dashboard with GraphQL subscriptions, Apollo, and React. Learn server, client, auth, and scaling patterns step-by-step.
GraphQL to REST Migration: A Practical, Low‑Risk Guide
Step-by-step guide to migrate APIs from GraphQL to REST: design mapping, caching, auth, rollout, and pitfalls to avoid.
REST API Endpoint Naming: Conventions That Scale
A practical guide to naming REST API endpoints: principles, patterns, examples, and a checklist for clear, consistent, and evolvable URLs.