Federation isn’t about merging your GraphQL services; it’s about composing them into a single, unified graph that clients can query as if it were one API.
Let’s say you have two GraphQL services: users-service and products-service.
users-service schema:
type User {
id: ID!
name: String!
email: String
}
type Query {
user(id: ID!): User
}
products-service schema:
type Product {
id: ID!
name: String!
price: Float!
ownerId: ID!
}
type Query {
product(id: ID!): Product
}
A client might want to query for a product and its owner’s name. With federation, you can achieve this without products-service knowing anything about users-service.
Here’s how you’d set up a gateway service using Apollo Federation:
-
Define the
Usertype inproducts-serviceas an entity: Inproducts-service, you need to tell the gateway thatUseris an entity it can reference.products-service/schema.graphql:extend type Query { product(id: ID!): Product } type Product @key(fields: "id") { # Mark Product as an entity id: ID! name: String! price: Float! ownerId: ID! } # Define User as a type that Product can reference, but don't implement its fields here type User @extends { id: ID! # Required to link }products-service/resolvers.js:// ... other resolvers Query: { product: (_, { id }) => { /* fetch product from DB */ }, }, Product: { // This resolver is only called when a query needs to resolve Product fields // *and* the gateway needs to fetch the full Product object by its ID. // It's *not* called when fetching a Product from a query that originated in products-service. __resolveReference(reference, { fetchUser }) { // The 'reference' object will contain the fields marked with @key. // In this case, it's { __typename: "Product", id: "..." } // You'd typically fetch the full product object here. // This resolver is not directly relevant for the User link, but shows how entities work. }, // This resolver is crucial for linking! owner: (product, _, { dataSources }) => { // product here is the product object fetched by the gateway or from the product query. // We use ownerId to fetch the User. return dataSources.usersAPI.getUser(product.ownerId); } } -
Define the
Producttype inusers-serviceas an entity: Similarly,users-serviceneeds to know aboutProductand how to retrieve it by its key.users-service/schema.graphql:type Query { user(id: ID!): User } type User @key(fields: "id") { # Mark User as an entity id: ID! name: String! email: String } # Define Product as a type that User can reference type Product @extends { id: ID! # Required to link }users-service/resolvers.js:// ... other resolvers Query: { user: (_, { id }, { dataSources }) => dataSources.usersAPI.getUser(id), }, User: { __resolveReference(reference, { fetchProduct }) { // The 'reference' object will contain { __typename: "User", id: "..." } // You'd typically fetch the full user object here. }, // This resolver is crucial for linking! products: (user, _, { dataSources }) => { // user here is the user object fetched by the gateway or from the user query. // We use user.id to fetch products owned by this user. return dataSources.productsAPI.getProductsByOwner(user.id); } } -
Configure the Gateway: The gateway acts as the entry point and orchestrates requests to the underlying services.
gateway/index.js:const { ApolloServer } = require('apollo-server'); const { ApolloGateway } = require('apollo-gateway'); const gateway = new ApolloGateway({ serviceList: [ { name: 'users', url: 'http://localhost:4001/graphql' }, { name: 'products', url: 'http://localhost:4002/graphql' }, ], }); const server = new ApolloServer({ gateway, // Subscriptions can also be configured here }); server.listen(4000).then(({ url }) => { console.log(`🚀 Gateway ready at ${url}`); });
Now, a client can make a query like this against the gateway:
query GetProductOwner($productId: ID!) {
product(id: $productId) {
id
name
owner { # This field comes from the 'owner' resolver in products-service
id
name # This field comes from the 'name' field of the User type, resolved by users-service
products { # This field comes from the 'products' resolver in users-service
id
name
}
}
}
}
The gateway receives this query. It sees product and knows products-service can handle it. It queries products-service for the product. Then, it sees the owner field. Because Product has an owner field that returns a User, and User is an entity defined in users-service, the gateway knows to ask users-service for the User data. It passes the ownerId from the product to users-service for a User entity lookup. The process continues recursively for nested fields like products.
Caching with Federation:
Caching in a federated architecture is typically handled at the gateway level. Apollo Gateway supports automatic caching for entities. When a query requests an entity (e.g., User @key(fields: "id")), the gateway can cache the response based on the entity’s key (id). Subsequent requests for the same entity can be served directly from the cache, reducing load on downstream services.
You can configure a cache provider like apollo-server-caching for the gateway:
const { ApolloServer } = require('apollo-server');
const { ApolloGateway } = require('apollo-gateway');
const { RESTDataSource } = require('apollo-datasource-rest');
const { InMemoryLRUCache } = require('apollo-server-caching'); // Or RedisCache, etc.
// ... serviceList definition ...
const gateway = new ApolloGateway({
serviceList,
buildService({ url }) {
return new RemoteGraphQLDataSource({
url,
cache: new InMemoryLRUCache(), // Configure cache here
});
},
});
const server = new ApolloServer({
gateway,
// ... other ApolloServer config ...
});
This InMemoryLRUCache will store responses for entities, keyed by their __typename and their @key fields. For instance, a User with id: "123" might be cached under a key like User:123.
Authentication and Authorization:
Auth in a federated system is a layered concern:
-
Gateway Authentication: The gateway can authenticate incoming requests. This typically involves validating a JWT or session token. If authentication fails, the request is rejected early. You’d implement this in the
ApolloServerconstructor for the gateway:const { ApolloServer } = require('apollo-server'); const { ApolloGateway } = require('apollo-gateway'); // ... const server = new ApolloServer({ gateway, context: ({ req }) => { // Authenticate the request and attach user info to context const token = req.headers.authorization || ''; const user = validateToken(token); // Your token validation logic return { user }; }, }); -
Service Authorization: Once authenticated, the gateway forwards the request (and the user context) to the appropriate downstream service. Each service is then responsible for authorizing the specific operation based on the user’s identity and permissions.
In the service’s resolvers, you’d access the user context:
users-service/resolvers.js:Query: { user: (_, { id }, { user, dataSources }) => { if (!user || !user.isAdmin) { // Example authorization check throw new Error('Unauthorized'); } return dataSources.usersAPI.getUser(id); }, },Or, more granularly, within the
Usertype’s fields:User: { email: (user, _, { user: currentUser }) => { if (user.id === currentUser.id || currentUser.isAdmin) { return user.email; } return null; // Mask email if not authorized }, // ... }The key insight for authorization is that each service must be able to verify the identity and permissions of the user making the request. The gateway doesn’t magically authorize everything; it passes the authenticated identity, and services enforce their own boundaries. A common pattern is to ensure that the
contextobject passed to each service’s resolvers contains the authenticated user’s details. This means the gateway needs to ensure that whatever authentication mechanism it uses injects this information into the context that is then passed down to the services.
The next challenge is handling complex data transformations and relationships across services when a single field requires data from multiple underlying services, or when you need to enforce fine-grained access control on specific fields across the entire graph.