Take your GraphQL Federation to the next level with advanced patterns for complex use cases.
Distributed Tracing
Trace ID Propagation
javascript
const { ApolloServer } = require('@apollo/server');
const server = new ApolloServer({
context: async ({ req }) => {
const traceId = req.headers['x-trace-id'] || generateTraceId();
return {
traceId,
// Propagate to downstream services
fetchOptions: {
headers: {
'X-Trace-ID': traceId,
},
},
};
},
});
// In resolvers
const resolvers = {
Query: {
user: async (_, { id }, { traceId, dataSources }) => {
const user = await dataSources.usersAPI.getUser(id, {
headers: { 'X-Trace-ID': traceId },
});
return user;
},
},
};Authentication in Federation
Gateway Authentication
javascript
// Gateway
const { ApolloGateway } = require('@apollo/gateway');
const gateway = new ApolloGateway({
async context({ req }) {
const token = req.headers.authorization || '';
const user = await authenticateToken(token);
return {
user,
// Pass user to subgraphs
headers: {
'X-User-Id': user.id,
'X-User-Role': user.role,
},
};
},
});Subgraph Authentication
javascript
// Subgraph
const server = new ApolloServer({
context: async ({ req }) => {
const userId = req.headers['x-user-id'];
if (!userId) {
throw new AuthenticationError('Unauthorized');
}
return {
userId,
userRole: req.headers['x-user-role'],
};
},
});Authorization
Field-Level Authorization
javascript
const resolvers = {
User: {
email: (user, _, { userRole }) => {
// Only admins can see emails
if (userRole !== 'admin') {
return null;
}
return user.email;
},
},
};Directive-Based Authorization
graphql
directive @auth(requires: Role) on FIELD_DEFINITION
enum Role {
ADMIN
USER
}
type User @key(fields: "id") {
id: ID!
email: String! @auth(requires: ADMIN)
name: String!
}
type Query {
adminData: String @auth(requires: ADMIN)
}javascript
const { defaultFieldResolver } = require('graphql');
const authDirective = (schema) => {
return mapSchema(schema, {
[MapperKind.FIELD]: (fieldConfig) => {
const authDirectives = getDirectives(
schema,
fieldConfig
).auth;
if (authDirectives) {
const { requires } = authDirectives;
const originalResolve = fieldConfig.resolve ||
defaultFieldResolver;
fieldConfig.resolve = function(
source,
args,
context,
info
) {
const userRole = context.userRole;
if (requires === 'ADMIN' && userRole !== 'admin') {
throw new AuthenticationError('Unauthorized');
}
return originalResolve(source, args, context, info);
};
}
return fieldConfig;
},
});
};Custom Directives
@uppercase Directive
graphql
directive @uppercase on FIELD_DEFINITION
type User @key(fields: "id") {
id: ID!
name: String! @uppercase
email: String!
}javascript
const uppercaseDirective = (schema) => {
return mapSchema(schema, {
[MapperKind.FIELD]: (fieldConfig) => {
const uppercaseDirectives = getDirectives(
schema,
fieldConfig
).uppercase;
if (uppercaseDirectives) {
const originalResolve = fieldConfig.resolve ||
defaultFieldResolver;
fieldConfig.resolve = async function(
source,
args,
context,
info
) {
const result = await originalResolve(
source,
args,
context,
info
);
return typeof result === 'string'
? result.toUpperCase()
: result;
};
}
return fieldConfig;
},
});
};Batch Loading
DataLoader Pattern
javascript
const DataLoader = require('dataloader');
class UsersDataSource {
constructor() {
this.userLoader = new DataLoader(async (userIds) => {
const users = await this.batchGetUsers(userIds);
return userIds.map(id =>
users.find(u => u.id === id) || null
);
});
}
async getUser(id) {
return this.userLoader.load(id);
}
async batchGetUsers(userIds) {
return await db.users.where('id').in(userIds);
}
}
const resolvers = {
Post: {
author: async (post, _, { dataSources }) => {
return dataSources.users.getUser(post.authorId);
},
},
};Cross-Subgraph Batching
javascript
// Gateway context
const gateway = new ApolloGateway({
context: async () => {
return {
loaders: {
user: new DataLoader(async (ids) => {
return fetchUsersByIds(ids);
}),
},
};
},
});
// In subgraph
const resolvers = {
Post: {
author: (post, _, { loaders }) => {
return loaders.user.load(post.authorId);
},
},
};Caching Strategies
Response Caching
javascript
const responseCachePlugin = require('@apollo/server-plugin-response-cache');
const server = new ApolloServer({
plugins: [
responseCachePlugin({
session: async (requestContext) => {
// Cache per user
return requestContext.request.http.headers.get(
'authorization'
);
},
}),
],
});
// Cache hint in schema
type Query {
user(id: ID!): User @cacheControl(maxAge: 60)
}Subgraph Caching
javascript
const { ApolloServer } = require('@apollo/server');
const server = new ApolloServer({
cache: new InMemoryLRUCache(),
// Or Redis
cache: new RedisCache({
host: 'redis',
port: 6379,
}),
});Schema Stitching vs Federation
When to Use Each
text
Schema Stitching:
- Single monolithic gateway
- Gateway transforms and combines schemas
- More control but more complexity
Federation:
- Declarative composition
- Subgraphs own their schemas
- Better for large teams
- Easier to maintainMigrating to Federation
Incremental Migration
javascript
// Step 1: Keep monolith, add federated services
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'monolith', url: 'http://monolith:4000' },
{ name: 'users', url: 'http://users:4001' },
],
}),
});
// Step 2: Move domains to subgraphs incrementally
// Step 3: Eventually decommission monolithMonitoring and Observability
Custom Metrics
javascript
const { ApolloServer } = require('@apollo/server');
const server = new ApolloServer({
plugins: [
{
requestDidStart: () => ({
willSendResponse: ({ response }) => {
const traceId = response.context.traceId;
metrics.record('graphql.request', {
traceId,
operation: response.request.operationName,
duration: response.body.kind === 'single'
? response.body.singleResult.extensions?.timing
: 0,
});
},
}),
},
],
});Performance Monitoring
javascript
const plugin = {
requestDidStart: () => ({
executionDidStart: () => ({
willResolveField({ info }) {
const start = Date.now();
return () => {
const duration = Date.now() - start;
metrics.record('graphql.field.resolve', {
fieldName: info.fieldName,
typeName: info.parentType.name,
duration,
});
};
},
}),
}),
};Best Practices
DOs
- Use DataLoader for batching
- Implement proper caching
- Monitor performance
- Handle errors gracefully
- Use custom directives wisely
- Test federated queries
- Version subgraphs
DON'Ts
- Create circular dependencies
- Over-fetch across services
- Ignore authentication
- Skip error handling
- Forget about monitoring
- Mix concerns
- Create tight coupling
Conclusion
Advanced GraphQL Federation patterns enable sophisticated architectures. Focus on performance with DataLoader, implement proper security, and monitor everything. Build scalable federated graphs that serve your teams and users well!