Build a Unified GraphQL API with Apollo Federation: A Step‑by‑Step Guide
Learn how to stitch multiple GraphQL services into one unified API with Apollo Federation. Follow a step‑by‑step guide to set up subgraphs, configure the gateway, validate entity resolution, and tune performance with caching.
02 Aug 2025, 18:11 UTC

Problem & Takeaway
When a single GraphQL endpoint grows too large, splitting it into micro‑services becomes inevitable. However, clients still expect a single schema. Apollo Federation solves this by allowing each service to expose entity types with @key directives, while an Apollo Gateway stitches them together at runtime. The result is a unified schema without sacrificing service autonomy.
Desired Outcome
- Two independent GraphQL subgraphs (User and Post services) each exposing
@keydirectives. - One Apollo Gateway that merges the subgraphs into a single schema.
- Verified entity resolution and introspection on the gateway.
- Optional caching enabled for improved latency.
Prerequisites
- Node.js 18+ installed on each service host.
- Basic knowledge of GraphQL SDL and resolver functions.
- Access to a network where the gateway can reach both subgraphs.
- Optional: Docker for containerized deployment.
Step 1: Create the User Subgraph
Define the User type with a unique @key field and expose _entities and _service for the gateway.
// user-service/index.js
const { ApolloServer, gql } = require('apollo-server');
const { buildFederatedSchema } = require('@apollo/federation');
const typeDefs = gql`
type User @key(fields: "id") {
id: ID!
name: String
}
extend type Query {
me: User
}
`;
const resolvers = {
Query: {
me: () => ({ id: "1", name: "Alice" })
},
User: {
__resolveReference: (obj) => ({ id: obj.id, name: "Alice" })
}
};
const server = new ApolloServer({
schema: buildFederatedSchema([{ typeDefs, resolvers }])
});
server.listen({ port: 4001 }).then(({ url }) => {
console.log(`User subgraph ready at ${url}`);
});
Step 2: Create the Post Subgraph
The Post service references the User entity via @requires and declares its own @key directive.
// post-service/index.js
const { ApolloServer, gql } = require('apollo-server');
const { buildFederatedSchema } = require('@apollo/federation');
const typeDefs = gql`
type Post @key(fields: "id") {
id: ID!
title: String
author: User @requires(fields: "id")
}
type User @extends @key(fields: "id") {
id: ID! @external
name: String @external
}
extend type Query {
posts: [Post]
}
`;
const resolvers = {
Query: {
posts: () => ([{ id: "101", title: "Federation Rocks", author: { id: "1" } }])
},
Post: {
author: (post) => ({ id: post.author.id })
}
};
const server = new ApolloServer({
schema: buildFederatedSchema([{ typeDefs, resolvers }])
});
server.listen({ port: 4002 }).then(({ url }) => {
console.log(`Post subgraph ready at ${url}`);
});
Step 3: Configure the Apollo Gateway
The gateway pulls the subgraph schemas and exposes a single endpoint. It also handles entity resolution automatically.
// gateway/index.js
const { ApolloServer } = require('apollo-server');
const { ApolloGateway } = require('@apollo/gateway');
const gateway = new ApolloGateway({
serviceList: [
{ name: 'user', url: 'http://localhost:4001/graphql' },
{ name: 'post', url: 'http://localhost:4002/graphql' }
],
buildService: ({ name, url }) => {
return new ApolloGateway({
serviceList: [{ name, url }]
});
},
// Enable caching for entity resolution
cache: {
type: 'in-memory',
maxAge: 60
}
});
const server = new ApolloServer({ gateway, subscriptions: false });
server.listen({ port: 4000 }).then(({ url }) => {
console.log(`Gateway ready at ${url}`);
});
Step 4: Validate the Schema
Run an introspection query on the gateway to confirm that _entities and _service are present.
curl -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ __schema { types { name } } }"}'
Expected check: the response includes _entities and _service in the type list.
Sample Query Across Services
curl -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ posts { id title author { id name } } }"}'
Expected result: a list of posts with nested author data resolved through the gateway.
Performance Tuning: Enable Gateway Caching
When the gateway is under load, caching the _entities lookup reduces round‑trips to subgraphs.
// In gateway/index.js, adjust cache settings
const gateway = new ApolloGateway({
serviceList: [...],
cache: {
type: 'in-memory',
maxAge: 120, // seconds
maxSize: 5000
}
});
After deployment, monitor apollo_gateway_request_latency metrics or use a tool like wrk to compare latency before and after caching.
Troubleshooting & Recovery
- Runtime error: unresolved entity – Verify that all
@keyfields match across services and that the gateway can reach each subgraph. - Missing _entities field – Ensure
buildFederatedSchemais used and that the subgraph includes the_entitiesintrospection resolver. - Performance degradation – Enable gateway logs (
debuglevel) to see where resolution stalls; consider increasing cache size or splitting hot paths into dedicated services. - Gateway crash on startup – Check Node.js version compatibility and that all dependencies are installed. Run
npm installin each service directory.
Limitations & Caveats
- Entity resolution errors surface only at runtime; static type checking cannot catch mismatched
@keydefinitions. - The gateway adds an extra hop; for very low‑latency use cases, consider a monolithic schema.
- Some GraphQL client libraries may not support the federation introspection queries; test with your client before production.
- Debugging distributed schemas requires inspecting logs from all services and the gateway.
Conclusion
Apollo Federation provides a pragmatic way to scale GraphQL services while keeping a single, discoverable schema. By following the steps above, you can deploy independent subgraphs, merge them with a gateway, validate entity resolution, and tune performance with caching. Regular introspection checks and monitoring will keep the federation healthy as services evolve.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.