GraphQL Federation Onboarding Template
Template for onboarding a service to a federated GraphQL graph: subgraph setup, entity definitions, resolver configuration, gateway integration, testing, deployment, and monitoring with code examples.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
This template guides a team through onboarding a new service to an existing federated GraphQL graph. Follow each section in order. Skip nothing — missing steps cause runtime errors in the gateway that are hard to debug.
1. Prerequisites
1.1 Before You Start
- Service has a GraphQL schema (SDL or code-first)
- Service exposes HTTP POST endpoint for GraphQL queries
- Service deployed to staging environment
- Team has access to the Apollo Studio / gateway repo
- Team has reviewed the API design guidelines
- Domain boundaries identified (what this subgraph owns)
1.2 Domain Ownership Declaration
Service name: {service-name}
Team: {team-name}
Domain: {domain description}
Types owned by this subgraph:
- {Type1}: full ownership
- {Type2}: full ownership
Types extended from other subgraphs:
- {Type3}: extends {other-service} with fields {field1}, {field2}
Entities (shared across subgraphs):
- {Entity1}: @key(fields: "id")
- {Entity2}: @key(fields: "id")
2. Subgraph Setup
2.1 Install Dependencies
# Using Apollo Server
npm install @apollo/server @apollo/subgraph graphql
# Using Mercurius (Fastify)
npm install mercurius mercurius-federation @mercurius/federation
2.2 Subgraph Schema Definition
# products-subgraph.graphqls
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@external", "@extends", "@shareable"])
type Product @key(fields: "id") {
id: ID!
name: String!
description: String!
price: Float!
currency: String!
categoryId: ID!
inStock: Boolean!
createdAt: DateTime!
updatedAt: DateTime!
}
type Category @key(fields: "id") {
id: ID!
name: String!
productCount: Int! @shareable
}
type Query {
product(id: ID!): Product
products(first: Int = 20, after: String): ProductConnection!
category(id: ID!): Category
}
type Mutation {
createProduct(input: CreateProductInput!): CreateProductPayload!
updateProduct(input: UpdateProductInput!): UpdateProductPayload!
}
type ProductConnection {
edges: [ProductEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type ProductEdge {
node: Product!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
scalar DateTime
2.3 Resolver Configuration
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { buildSubgraphSchema } from '@apollo/subgraph';
const typeDefs = `#graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@external", "@extends"])
type Product @key(fields: "id") {
id: ID!
name: String!
price: Float!
}
type Query {
product(id: ID!): Product
}
`;
const resolvers = {
Query: {
product: (_, { id }, context) => {
return context.dataSources.productDb.findById(id);
},
},
// Entity resolver — called when gateway resolves a Product by key
Product: {
__resolveReference: (reference, context) => {
return context.dataSources.productDb.findById(reference.id);
},
},
};
const server = new ApolloServer({
schema: buildSubgraphSchema([{ typeDefs, resolvers }]),
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4001 },
context: async ({ req }) => ({
dataSources: createDataSources(req),
user: authenticateUser(req),
}),
});
3. Entity Resolution
3.1 Entity Resolver Rules
- Every type with
@keymust have__resolveReference - The resolver receives the key fields and returns the full entity
- If the entity is not found, return null (not an error)
- Cache entity lookups with DataLoader per request
- Entity resolvers must be fast (< 50ms p95)
import DataLoader from 'dataloader';
const resolvers = {
Product: {
__resolveReference: (reference, context) => {
// reference = { id: "123", __typename: "Product" }
return context.loaders.productById.load(reference.id);
},
},
Category: {
__resolveReference: (reference, context) => {
return context.loaders.categoryById.load(reference.id);
},
},
};
function createLoaders(db) {
return {
productById: new DataLoader(async (ids) => {
const products = await db.products.findMany({ where: { id: { in: ids } } });
return ids.map(id => products.find(p => p.id === id) || null);
}),
categoryById: new DataLoader(async (ids) => {
const categories = await db.categories.findMany({ where: { id: { in: ids } } });
return ids.map(id => categories.find(c => c.id === id) || null);
}),
};
}
3.2 Extending Entities from Other Subgraphs
# reviews-subgraph.graphqls
# Extends Product entity from Products subgraph
extend type Product @key(fields: "id") {
id: ID! @external
reviews: [Review!]!
averageRating: Float!
reviewCount: Int!
}
type Review @key(fields: "id") {
id: ID!
productId: ID!
userId: ID!
rating: Int!
comment: String!
createdAt: DateTime!
}
type Query {
reviews(productId: ID!, first: Int = 10, after: String): ReviewConnection!
}
// Reviews subgraph resolvers
const resolvers = {
Product: {
// Resolve extended fields when Product is accessed via gateway
reviews: (parent, _, context) => {
return context.dataSources.reviewDb.findByProduct(parent.id);
},
averageRating: (parent, _, context) => {
return context.dataSources.reviewDb.getAverageRating(parent.id);
},
reviewCount: (parent, _, context) => {
return context.dataSources.reviewDb.getCount(parent.id);
},
},
};
4. Gateway Integration
4.1 Gateway Configuration
// gateway/index.js
import { ApolloGateway } from '@apollo/gateway';
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const gateway = new ApolloGateway({
serviceList: [
{ name: 'products', url: 'https://products-service.internal/graphql' },
{ name: 'reviews', url: 'https://reviews-service.internal/graphql' },
{ name: 'users', url: 'https://users-service.internal/graphql' },
{ name: 'orders', url: 'https://orders-service.internal/graphql' },
],
// Poll for schema changes every 10 seconds in staging
debug: process.env.NODE_ENV !== 'production',
});
const server = new ApolloServer({
gateway,
// Disable subscriptions (not supported in federation)
subscriptions: false,
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
context: async ({ req }) => ({
user: authenticateUser(req),
correlationId: req.headers['x-correlation-id'],
}),
});
4.2 Supergraph Composition
# supergraph.yaml
federation_version: =2.0.0
subgraphs:
products:
routing_url: https://products-service.internal/graphql
schema:
file: ./subgraphs/products/products.graphqls
reviews:
routing_url: https://reviews-service.internal/graphql
schema:
file: ./subgraphs/reviews/reviews.graphqls
users:
routing_url: https://users-service.internal/graphql
schema:
file: ./subgraphs/users/users.graphqls
orders:
routing_url: https://orders-service.internal/graphql
schema:
file: ./subgraphs/orders/orders.graphqls
# Compose the supergraph schema
rover supergraph compose --config supergraph.yaml > supergraph-schema.graphql
# Validate composition
rover subgraph check products@production \
--schema ./subgraphs/products/products.graphqls \
--routing-url https://products-service.internal/graphql
5. Testing
5.1 Subgraph Tests
// Test entity resolution
describe('Product entity resolver', () => {
it('resolves a product by ID', async () => {
const result = await executeOperation({
query: `{ product(id: "1") { id name price } }`,
});
expect(result.data.product).toEqual({
id: '1',
name: 'Widget',
price: 9.99,
});
});
it('returns null for non-existent product', async () => {
const result = await executeOperation({
query: `{ product(id: "999") { id name } }`,
});
expect(result.data.product).toBeNull();
});
});
// Test __resolveReference directly
describe('Product __resolveReference', () => {
it('resolves entity reference', async () => {
const result = await resolvers.Product.__resolveReference(
{ id: '1', __typename: 'Product' },
testContext
);
expect(result.id).toBe('1');
expect(result.name).toBeDefined();
});
});
5.2 Federation Integration Tests
// Test cross-subgraph queries
describe('Federated queries', () => {
it('resolves product with reviews across subgraphs', async () => {
const result = await gatewayExecute({
query: `
query {
product(id: "1") {
id
name
reviews {
rating
comment
}
averageRating
}
}
`,
});
expect(result.data.product.id).toBe('1');
expect(result.data.product.name).toBeDefined();
expect(result.data.product.reviews).toBeInstanceOf(Array);
expect(result.data.product.averageRating).toBeGreaterThan(0);
});
});
5.3 Testing Checklist
- Subgraph resolves all own types correctly
-
__resolveReferenceworks for all entities - Extended fields resolve correctly
- Cross-subgraph queries return correct data
- Null handling: missing entity returns null, not error
- Error handling: subgraph errors propagate to gateway
- Authentication context passes through gateway
- DataLoader batching works within subgraph
6. Deployment
6.1 Deployment Checklist
- Subgraph deployed to staging
- Schema composed with
rover supergraph compose - Composition validated (no conflicts, no circular dependencies)
- Gateway updated with new service URL
- Integration tests pass in staging
- Performance benchmarks run (p95 < 200ms for cross-subgraph queries)
- Monitoring dashboards configured
- Alerting rules set up
- Rollback plan documented
6.2 Deployment Steps
1. Deploy subgraph service to staging
- Verify health check passes
- Verify GraphQL endpoint responds
- Run subgraph test suite
2. Compose supergraph with new subgraph
- rover supergraph compose --config supergraph.yaml
- Verify no composition errors
- Check for schema conflicts
3. Update gateway configuration
- Add new service to serviceList
- Deploy gateway to staging
- Run integration tests
4. Verify in staging
- Test cross-subgraph queries
- Check gateway logs for errors
- Monitor subgraph health
5. Deploy to production
- Deploy subgraph service
- Update gateway serviceList
- Monitor error rates for 1 hour
- Verify query success rate > 99.9%
7. Monitoring
7.1 Key Metrics
Metric | Source | Alert Threshold
────────────────────────────────┼───────────────┼──────────────────
Subgraph request rate | Gateway | < 10 req/s (check)
Subgraph error rate | Gateway | > 1%
Subgraph latency p95 | Gateway | > 200ms
Entity resolver latency p95 | Subgraph | > 50ms
Gateway query latency p95 | Gateway | > 500ms
Schema composition failures | CI/CD | Any
Subgraph health check failures | Gateway > 3 consecutive
Cross-subgraph query errors | Gateway | > 0.5%
7.2 Monitoring Setup
// Subgraph health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
subgraph: 'products',
version: process.env.APP_VERSION,
schemaHash: currentSchemaHash,
});
});
// Apollo Studio integration for schema tracking
const server = new ApolloServer({
schema: buildSubgraphSchema([{ typeDefs, resolvers }]),
plugins: [
process.env.NODE_ENV === 'production' && ApolloServerPluginInlineTrace(),
],
});
FAQ
How do I share custom scalars across subgraphs?
Define custom scalars (DateTime, JSON, URL) in a shared npm package. Each subgraph imports the scalar definition and resolver. The gateway automatically merges scalar definitions during composition. If scalars are defined differently in two subgraphs, composition fails. Use a shared package like @company/graphql-scalars to ensure consistency.
What happens if one subgraph is down?
The gateway returns partial results for queries that can be resolved by available subgraphs. If a query requires data from the down subgraph, the gateway returns an error for that field while still resolving other fields. Use @requires directives carefully — if a required field cannot be resolved, the entire entity resolution fails. Configure circuit breakers in the gateway to fail fast rather than waiting for timeouts.
How do I handle authentication in a federated graph?
Authenticate at the gateway level. Extract the JWT from the Authorization header in the gateway context. Pass the decoded user object to subgraphs via the x-user header or a custom context extension. Each subgraph receives the user in its context and performs field-level authorization. Do not authenticate in each subgraph independently — that causes redundant work and inconsistent behavior.
Can I use subscriptions with federation?
Standard Apollo Federation does not support subscriptions through the gateway. If you need real-time updates, run a separate subscription endpoint outside the gateway. Clients connect to the subscription endpoint directly and to the gateway for queries and mutations. Use a pub/sub system (Redis, NATS) to share events between subgraphs and the subscription endpoint. Apollo is working on native subscription support in future federation versions.
How do I debug a failing entity resolution?
Check three things: (1) Is __resolveReference implemented for the entity type? (2) Does the resolver return null when the entity is not found? (3) Is the key field in the reference object spelled correctly? Enable query tracing in Apollo Studio to see which subgraph resolved which field. Check gateway logs for ENTITY_RESOLUTION_FAILED errors. Test the entity resolver in isolation with a direct subgraph query before testing through the gateway.
See Also
Related Resources
GraphQL Schema Review Checklist
Checklist for reviewing GraphQL schemas: naming conventions, type design, pagination, error handling, security, performance, deprecation, and federation readiness with code examples and validation rules.
DocGraphQL API Design Guideline
Internal guidelines for designing GraphQL APIs: schema structure, naming, mutation patterns, error handling, pagination, authentication, rate limiting, versioning, and federation rules with code examples.
DocGraphQL Deprecation Policy Template
Policy template for deprecating GraphQL fields, types, arguments, and enum values safely. Includes deprecation timeline, communication plan, usage tracking, removal criteria, and migration examples.
RecipeSet Up a GraphQL Federation Gateway with Apollo
Compose multiple GraphQL services into a single federated supergraph using Apollo Federation and a gateway that routes queries across subgraphs
PatternGraphQL Federated Entity Pattern
Share entity types across federated GraphQL services so the gateway can resolve fields from multiple subgraphs transparently.
GuideGraphQL Federation in Production
Run federated GraphQL in production with confidence. Covers subgraph composition, gateway deployment, entity resolution, schema coordination, observability, and failure handling.