GraphQL Federation
GraphQL Federation enables you to compose a unified API from multiple GraphQL services, perfect for microservices architectures.Learning Objectives:
- Understand GraphQL Federation architecture
- Implement Apollo Federation with Node.js
- Master schema composition and entities
- Build federated queries and mutations
- Handle authentication and authorization
Why GraphQL Federation?
Copy
┌─────────────────────────────────────────────────────────────────────────────┐
│ THE PROBLEM WITH REST IN MICROSERVICES │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Client wants: User with their orders and order products │
│ │
│ REST APPROACH (Multiple round trips): │
│ ───────────────────────────────────── │
│ │
│ 1. GET /users/123 → { name, email } │
│ 2. GET /users/123/orders → [{ id: 'o1' }, { id: 'o2' }] │
│ 3. GET /orders/o1 → { items: ['p1', 'p2'] } │
│ 4. GET /orders/o2 → { items: ['p3'] } │
│ 5. GET /products/p1 → { name, price } │
│ 6. GET /products/p2 → { name, price } │
│ 7. GET /products/p3 → { name, price } │
│ │
│ Total: 7 HTTP requests! 🐢 │
│ │
│ ═══════════════════════════════════════════════════════════════════════ │
│ │
│ GRAPHQL FEDERATION (Single request): │
│ ──────────────────────────────────── │
│ │
│ query { │
│ user(id: "123") { │
│ name │
│ email │
│ orders { │
│ id │
│ items { │
│ product { name, price } │
│ quantity │
│ } │
│ } │
│ } │
│ } │
│ │
│ Total: 1 request! Gateway handles federation 🚀 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Federation Architecture
Copy
┌─────────────────────────────────────────────────────────────────────────────┐
│ APOLLO FEDERATION ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ Clients │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────┐ │
│ │ Apollo Gateway │ │
│ │ ┌──────────────────────────┐ │ │
│ │ │ Supergraph Schema │ │ │
│ │ │ (Composed from all) │ │ │
│ │ └──────────────────────────┘ │ │
│ │ ┌──────────────────────────┐ │ │
│ │ │ Query Planner │ │ │
│ │ │ (Optimizes execution) │ │ │
│ │ └──────────────────────────┘ │ │
│ └──────────────┬────────────────┘ │
│ │ │
│ ┌───────────────────────┼───────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ Users Subgraph│ │Orders Subgraph│ │Product Subgraph│ │
│ │ │ │ │ │ │ │
│ │ type User │ │ type Order │ │ type Product │ │
│ │ @key(id) │ │ @key(id) │ │ @key(id) │ │
│ │ │ │ │ │ │ │
│ │ Users DB │ │ Orders DB │ │ Products DB │ │
│ └───────────────┘ └───────────────┘ └───────────────┘ │
│ │
│ KEY CONCEPTS: │
│ • Subgraph: Individual GraphQL service with part of the schema │
│ • Supergraph: Composed schema from all subgraphs │
│ • Entity: Type that can be resolved across subgraphs (@key directive) │
│ • Query Planner: Optimizes how to fetch data from multiple subgraphs │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Implementing Subgraphs
Users Subgraph
Copy
// users-subgraph/index.js
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
const { buildSubgraphSchema } = require('@apollo/subgraph');
const { gql } = require('graphql-tag');
const typeDefs = gql`
extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@external", "@requires"])
type Query {
user(id: ID!): User
users: [User!]!
me: User
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
}
# Entity - can be referenced from other subgraphs
type User @key(fields: "id") {
id: ID!
email: String!
name: String!
avatar: String
createdAt: String!
}
input CreateUserInput {
email: String!
name: String!
password: String!
}
input UpdateUserInput {
name: String
avatar: String
}
`;
// In-memory data store (use real DB in production)
const users = [
{ id: '1', email: '[email protected]', name: 'Alice', createdAt: '2024-01-01' },
{ id: '2', email: '[email protected]', name: 'Bob', createdAt: '2024-01-02' }
];
const resolvers = {
Query: {
user: (_, { id }) => users.find(u => u.id === id),
users: () => users,
me: (_, __, { user }) => user // From auth context
},
Mutation: {
createUser: (_, { input }) => {
const newUser = {
id: String(users.length + 1),
...input,
createdAt: new Date().toISOString()
};
users.push(newUser);
return newUser;
},
updateUser: (_, { id, input }) => {
const user = users.find(u => u.id === id);
if (!user) throw new Error('User not found');
Object.assign(user, input);
return user;
}
},
User: {
// Reference resolver - called when other subgraphs need to resolve a User
__resolveReference: (userRef) => {
return users.find(u => u.id === userRef.id);
}
}
};
const server = new ApolloServer({
schema: buildSubgraphSchema({ typeDefs, resolvers })
});
startStandaloneServer(server, { listen: { port: 4001 } })
.then(({ url }) => console.log(`Users subgraph ready at ${url}`));
Orders Subgraph
Copy
// orders-subgraph/index.js
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
const { buildSubgraphSchema } = require('@apollo/subgraph');
const { gql } = require('graphql-tag');
const typeDefs = gql`
extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@external", "@requires", "@extends"])
type Query {
order(id: ID!): Order
orders(userId: ID): [Order!]!
}
type Mutation {
createOrder(input: CreateOrderInput!): Order!
updateOrderStatus(id: ID!, status: OrderStatus!): Order!
}
type Order @key(fields: "id") {
id: ID!
status: OrderStatus!
items: [OrderItem!]!
total: Float!
createdAt: String!
# Reference to User entity (defined in Users subgraph)
user: User!
}
type OrderItem {
product: Product!
quantity: Int!
price: Float!
}
# Extend User type from Users subgraph
type User @key(fields: "id") {
id: ID! @external
# Add orders field to User type
orders: [Order!]!
}
# Reference to Product entity (defined in Products subgraph)
type Product @key(fields: "id") {
id: ID! @external
}
enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
CANCELLED
}
input CreateOrderInput {
userId: ID!
items: [OrderItemInput!]!
}
input OrderItemInput {
productId: ID!
quantity: Int!
}
`;
// Sample data
const orders = [
{
id: 'o1',
userId: '1',
status: 'DELIVERED',
items: [
{ productId: 'p1', quantity: 2, price: 29.99 },
{ productId: 'p2', quantity: 1, price: 49.99 }
],
total: 109.97,
createdAt: '2024-01-10'
},
{
id: 'o2',
userId: '1',
status: 'SHIPPED',
items: [
{ productId: 'p3', quantity: 1, price: 199.99 }
],
total: 199.99,
createdAt: '2024-01-15'
}
];
const resolvers = {
Query: {
order: (_, { id }) => orders.find(o => o.id === id),
orders: (_, { userId }) =>
userId ? orders.filter(o => o.userId === userId) : orders
},
Mutation: {
createOrder: async (_, { input }, { dataSources }) => {
// Fetch product prices from Products subgraph
const items = await Promise.all(
input.items.map(async item => {
const product = await dataSources.productsAPI.getProduct(item.productId);
return {
productId: item.productId,
quantity: item.quantity,
price: product.price
};
})
);
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const newOrder = {
id: `o${orders.length + 1}`,
userId: input.userId,
status: 'PENDING',
items,
total,
createdAt: new Date().toISOString()
};
orders.push(newOrder);
return newOrder;
},
updateOrderStatus: (_, { id, status }) => {
const order = orders.find(o => o.id === id);
if (!order) throw new Error('Order not found');
order.status = status;
return order;
}
},
Order: {
__resolveReference: (orderRef) => orders.find(o => o.id === orderRef.id),
// Return reference stub - Gateway will resolve via Users subgraph
user: (order) => ({ __typename: 'User', id: order.userId }),
items: (order) => order.items.map(item => ({
...item,
product: { __typename: 'Product', id: item.productId }
}))
},
// Extend User type with orders
User: {
orders: (user) => orders.filter(o => o.userId === user.id)
}
};
const server = new ApolloServer({
schema: buildSubgraphSchema({ typeDefs, resolvers })
});
startStandaloneServer(server, { listen: { port: 4002 } })
.then(({ url }) => console.log(`Orders subgraph ready at ${url}`));
Products Subgraph
Copy
// products-subgraph/index.js
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
const { buildSubgraphSchema } = require('@apollo/subgraph');
const { gql } = require('graphql-tag');
const typeDefs = gql`
extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable"])
type Query {
product(id: ID!): Product
products(category: String): [Product!]!
searchProducts(query: String!): [Product!]!
}
type Mutation {
createProduct(input: CreateProductInput!): Product!
updateProduct(id: ID!, input: UpdateProductInput!): Product!
updateInventory(id: ID!, quantity: Int!): Product!
}
type Product @key(fields: "id") {
id: ID!
name: String!
description: String
price: Float!
category: String!
inventory: Int!
images: [String!]!
reviews: [Review!]!
averageRating: Float @shareable
}
type Review {
id: ID!
userId: ID!
rating: Int!
comment: String
createdAt: String!
}
input CreateProductInput {
name: String!
description: String
price: Float!
category: String!
inventory: Int!
images: [String!]
}
input UpdateProductInput {
name: String
description: String
price: Float
category: String
images: [String!]
}
`;
const products = [
{
id: 'p1',
name: 'Wireless Headphones',
description: 'High-quality wireless headphones',
price: 29.99,
category: 'Electronics',
inventory: 100,
images: ['/images/headphones.jpg'],
reviews: [
{ id: 'r1', userId: '1', rating: 5, comment: 'Great sound!', createdAt: '2024-01-05' }
]
},
{
id: 'p2',
name: 'Laptop Stand',
description: 'Ergonomic laptop stand',
price: 49.99,
category: 'Accessories',
inventory: 50,
images: ['/images/stand.jpg'],
reviews: []
},
{
id: 'p3',
name: 'Mechanical Keyboard',
description: 'RGB mechanical keyboard',
price: 199.99,
category: 'Electronics',
inventory: 25,
images: ['/images/keyboard.jpg'],
reviews: [
{ id: 'r2', userId: '2', rating: 4, comment: 'Nice typing experience', createdAt: '2024-01-12' }
]
}
];
const resolvers = {
Query: {
product: (_, { id }) => products.find(p => p.id === id),
products: (_, { category }) =>
category ? products.filter(p => p.category === category) : products,
searchProducts: (_, { query }) =>
products.filter(p =>
p.name.toLowerCase().includes(query.toLowerCase()) ||
p.description?.toLowerCase().includes(query.toLowerCase())
)
},
Mutation: {
createProduct: (_, { input }) => {
const newProduct = {
id: `p${products.length + 1}`,
...input,
images: input.images || [],
reviews: []
};
products.push(newProduct);
return newProduct;
},
updateProduct: (_, { id, input }) => {
const product = products.find(p => p.id === id);
if (!product) throw new Error('Product not found');
Object.assign(product, input);
return product;
},
updateInventory: (_, { id, quantity }) => {
const product = products.find(p => p.id === id);
if (!product) throw new Error('Product not found');
product.inventory = quantity;
return product;
}
},
Product: {
__resolveReference: (productRef) => products.find(p => p.id === productRef.id),
averageRating: (product) => {
if (product.reviews.length === 0) return null;
const sum = product.reviews.reduce((acc, r) => acc + r.rating, 0);
return sum / product.reviews.length;
}
}
};
const server = new ApolloServer({
schema: buildSubgraphSchema({ typeDefs, resolvers })
});
startStandaloneServer(server, { listen: { port: 4003 } })
.then(({ url }) => console.log(`Products subgraph ready at ${url}`));
Apollo Gateway
Copy
// gateway/index.js
const { ApolloServer } = require('@apollo/server');
const { expressMiddleware } = require('@apollo/server/express4');
const { ApolloGateway, IntrospectAndCompose, RemoteGraphQLDataSource } = require('@apollo/gateway');
const express = require('express');
const cors = require('cors');
const jwt = require('jsonwebtoken');
// Custom data source for authentication
class AuthenticatedDataSource extends RemoteGraphQLDataSource {
willSendRequest({ request, context }) {
// Forward auth token to subgraphs
if (context.token) {
request.http.headers.set('authorization', context.token);
}
// Forward user info
if (context.user) {
request.http.headers.set('x-user-id', context.user.id);
request.http.headers.set('x-user-role', context.user.role);
}
}
}
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'users', url: 'http://localhost:4001/graphql' },
{ name: 'orders', url: 'http://localhost:4002/graphql' },
{ name: 'products', url: 'http://localhost:4003/graphql' }
]
}),
buildService({ name, url }) {
return new AuthenticatedDataSource({ url });
}
});
const server = new ApolloServer({
gateway,
plugins: [
{
async requestDidStart() {
return {
async didResolveOperation({ request, document, operationName }) {
console.log(`Operation: ${operationName}`);
},
async executionDidStart() {
return {
async executionDidEnd(err) {
if (err) {
console.error('Execution error:', err);
}
}
};
}
};
}
}
]
});
async function startServer() {
await server.start();
const app = express();
app.use(cors());
app.use(express.json());
// Authentication middleware
app.use('/graphql', (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (token) {
try {
const user = jwt.verify(token, process.env.JWT_SECRET);
req.user = user;
} catch (err) {
// Invalid token - continue without user
}
}
next();
});
app.use(
'/graphql',
expressMiddleware(server, {
context: async ({ req }) => ({
token: req.headers.authorization,
user: req.user
})
})
);
app.listen(4000, () => {
console.log('🚀 Gateway ready at http://localhost:4000/graphql');
});
}
startServer().catch(console.error);
Supergraph Configuration (Alternative to IntrospectAndCompose)
Copy
# supergraph.yaml - For production with Apollo Studio
federation_version: =2.0
subgraphs:
users:
routing_url: http://users-service:4001/graphql
schema:
subgraph_url: http://users-service:4001/graphql
orders:
routing_url: http://orders-service:4002/graphql
schema:
subgraph_url: http://orders-service:4002/graphql
products:
routing_url: http://products-service:4003/graphql
schema:
subgraph_url: http://products-service:4003/graphql
Advanced Federation Patterns
Entity References and @requires
Copy
# In Reviews subgraph - needs product name from Products subgraph
type Review @key(fields: "id") {
id: ID!
rating: Int!
comment: String
product: Product!
}
# Extend Product with additional fields
type Product @key(fields: "id") {
id: ID! @external
name: String! @external
# Computed field that requires external data
reviewSummary: ReviewSummary @requires(fields: "name")
}
type ReviewSummary {
productName: String! # Comes from @requires
averageRating: Float!
totalReviews: Int!
}
Copy
// Reviews subgraph resolver
const resolvers = {
Product: {
// @requires means 'name' will be fetched before this resolver runs
reviewSummary: (product) => {
const productReviews = reviews.filter(r => r.productId === product.id);
return {
productName: product.name, // Available due to @requires
averageRating: productReviews.reduce((sum, r) => sum + r.rating, 0) / productReviews.length,
totalReviews: productReviews.length
};
}
}
};
Compound Keys
Copy
# Entity with compound key
type CartItem @key(fields: "userId productId") {
userId: ID!
productId: ID!
quantity: Int!
addedAt: String!
}
Interface Entities
Copy
# Shared interface across subgraphs
interface Node {
id: ID!
}
type User implements Node @key(fields: "id") {
id: ID!
name: String!
}
type Product implements Node @key(fields: "id") {
id: ID!
name: String!
}
# Query that returns any Node
type Query {
node(id: ID!): Node
}
Error Handling in Federation
Copy
// error-handling.js - Federation-aware error handling
const { GraphQLError } = require('graphql');
// Custom error classes
class NotFoundError extends GraphQLError {
constructor(entity, id) {
super(`${entity} with id '${id}' not found`, {
extensions: {
code: 'NOT_FOUND',
entity,
id
}
});
}
}
class UnauthorizedError extends GraphQLError {
constructor(message = 'Not authorized') {
super(message, {
extensions: {
code: 'UNAUTHORIZED'
}
});
}
}
class ValidationError extends GraphQLError {
constructor(message, field) {
super(message, {
extensions: {
code: 'VALIDATION_ERROR',
field
}
});
}
}
// Error formatting in gateway
const formatError = (error) => {
// Log all errors
console.error('GraphQL Error:', error);
// Don't expose internal errors to clients
if (error.extensions?.code === 'INTERNAL_SERVER_ERROR') {
return new GraphQLError('Internal server error', {
extensions: { code: 'INTERNAL_SERVER_ERROR' }
});
}
return error;
};
// Subgraph error handling
const resolvers = {
Query: {
user: async (_, { id }, context) => {
// Authorization check
if (!context.user) {
throw new UnauthorizedError();
}
const user = await findUser(id);
if (!user) {
throw new NotFoundError('User', id);
}
return user;
}
},
Mutation: {
createUser: async (_, { input }) => {
// Validation
if (!input.email.includes('@')) {
throw new ValidationError('Invalid email format', 'email');
}
// Check for duplicates
const existing = await findUserByEmail(input.email);
if (existing) {
throw new ValidationError('Email already in use', 'email');
}
return createUser(input);
}
}
};
// Partial failures handling
const userResolver = {
orders: async (user) => {
try {
return await orderService.getByUserId(user.id);
} catch (error) {
// Log error but don't fail the whole query
console.error(`Failed to fetch orders for user ${user.id}:`, error);
// Return null or empty array instead of failing
return null; // Client will see orders: null
}
}
};
module.exports = { NotFoundError, UnauthorizedError, ValidationError, formatError };
Authentication & Authorization
Copy
// auth.js - Authentication across federated services
const { GraphQLError } = require('graphql');
const jwt = require('jsonwebtoken');
// Authentication directive
const authDirective = (directiveName) => {
return {
authDirectiveTypeDefs: `directive @${directiveName}(requires: Role = USER) on FIELD_DEFINITION`,
authDirectiveTransformer: (schema) => {
// Transform schema to add auth checks
// In practice, use @graphql-tools/utils
}
};
};
// Context creation with user
const createContext = async ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return { user: null };
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Optionally fetch full user from Users subgraph
// const user = await usersService.getUser(decoded.userId);
return {
user: {
id: decoded.userId,
email: decoded.email,
role: decoded.role
},
token
};
} catch (error) {
return { user: null };
}
};
// Field-level authorization in resolvers
const resolvers = {
User: {
// Only accessible to the user themselves or admins
email: (user, _, context) => {
if (!context.user) {
throw new GraphQLError('Not authenticated');
}
if (context.user.id !== user.id && context.user.role !== 'ADMIN') {
throw new GraphQLError('Not authorized to view email');
}
return user.email;
},
// Private field - only for the user
orders: (user, _, context) => {
if (!context.user || context.user.id !== user.id) {
throw new GraphQLError('Not authorized to view orders');
}
return getOrdersForUser(user.id);
}
},
Mutation: {
// Admin only mutation
deleteUser: (_, { id }, context) => {
if (!context.user || context.user.role !== 'ADMIN') {
throw new GraphQLError('Admin access required');
}
return deleteUser(id);
}
}
};
// Permission matrix for complex authorization
const permissions = {
User: {
id: ['*'], // Everyone can see
name: ['*'], // Everyone can see
email: ['self', 'admin'], // Only self or admin
orders: ['self', 'admin'], // Only self or admin
creditCard: ['self'] // Only self
},
Order: {
'*': ['owner', 'admin'], // Only owner or admin can see any field
}
};
function checkPermission(entity, field, context, record) {
const allowed = permissions[entity]?.[field] || permissions[entity]?.['*'] || ['*'];
if (allowed.includes('*')) return true;
if (allowed.includes('self') && context.user?.id === record.userId) return true;
if (allowed.includes('owner') && context.user?.id === record.ownerId) return true;
if (allowed.includes('admin') && context.user?.role === 'ADMIN') return true;
return false;
}
module.exports = { createContext, permissions, checkPermission };
Testing Federation
Copy
// federation.test.js
const { ApolloServer } = require('@apollo/server');
const { ApolloGateway, LocalGraphQLDataSource } = require('@apollo/gateway');
const { buildSubgraphSchema } = require('@apollo/subgraph');
describe('Federation Integration Tests', () => {
let gateway;
beforeAll(async () => {
// Create mock subgraphs
const usersSchema = buildSubgraphSchema(require('./users-subgraph/schema'));
const ordersSchema = buildSubgraphSchema(require('./orders-subgraph/schema'));
const productsSchema = buildSubgraphSchema(require('./products-subgraph/schema'));
gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'users', url: 'http://localhost:4001' },
{ name: 'orders', url: 'http://localhost:4002' },
{ name: 'products', url: 'http://localhost:4003' }
]
}),
buildService({ name }) {
const schemas = { users: usersSchema, orders: ordersSchema, products: productsSchema };
return new LocalGraphQLDataSource(schemas[name]);
}
});
});
test('federated query across subgraphs', async () => {
const server = new ApolloServer({ gateway });
const result = await server.executeOperation({
query: `
query UserWithOrders($userId: ID!) {
user(id: $userId) {
id
name
orders {
id
total
items {
product {
name
price
}
quantity
}
}
}
}
`,
variables: { userId: '1' }
});
expect(result.body.singleResult.errors).toBeUndefined();
expect(result.body.singleResult.data.user).toMatchObject({
id: '1',
name: 'Alice',
orders: expect.arrayContaining([
expect.objectContaining({
items: expect.arrayContaining([
expect.objectContaining({
product: expect.objectContaining({
name: expect.any(String)
})
})
])
})
])
});
});
test('entity resolution works correctly', async () => {
const server = new ApolloServer({ gateway });
// Query order and verify user is resolved
const result = await server.executeOperation({
query: `
query Order($orderId: ID!) {
order(id: $orderId) {
id
user {
name
email
}
}
}
`,
variables: { orderId: 'o1' }
});
expect(result.body.singleResult.data.order.user.name).toBeDefined();
});
});
Interview Questions
Q1: What is GraphQL Federation and how does it work?
Q1: What is GraphQL Federation and how does it work?
Answer:Federation: Composing a unified GraphQL API from multiple services.Key components:
- Subgraphs: Individual GraphQL services
- Gateway: Entry point that composes supergraph
- Supergraph: Combined schema from all subgraphs
- Entities: Types that can span multiple subgraphs
- Each subgraph defines its portion of the schema
- Gateway fetches schemas and composes supergraph
- Client sends query to gateway
- Gateway plans execution across subgraphs
- Results are combined and returned
@key: Defines entity’s unique identifier@external: Field defined in another subgraph@requires: Field needs external data
Q2: How do entities work in Federation?
Q2: How do entities work in Federation?
Answer:Entity: A type that can be resolved across multiple subgraphs.Definition:Reference resolution:How references work:
Copy
type User @key(fields: "id") {
id: ID!
name: String!
}
Copy
User: {
__resolveReference: (userRef) => {
// userRef = { __typename: 'User', id: '123' }
return findUserById(userRef.id);
}
}
- Orders subgraph returns
{ __typename: 'User', id: '1' } - Gateway sees it needs User fields
- Gateway calls Users subgraph’s
__resolveReference - Full User data is returned
Q3: What's the difference between @external and @requires?
Q3: What's the difference between @external and @requires?
Answer:@external: Marks a field as defined in another subgraph.@requires: Indicates a field needs external data to resolve.Execution:
Copy
type Product @key(fields: "id") {
id: ID! @external
name: String! @external
}
Copy
type Product @key(fields: "id") {
id: ID! @external
price: Float! @external
# This field needs 'price' from Products subgraph
priceWithTax: Float! @requires(fields: "price")
}
- Gateway fetches
pricefrom Products subgraph - Passes it to this subgraph’s resolver
- Resolver uses it to compute
priceWithTax
Q4: How do you handle authentication in Federation?
Q4: How do you handle authentication in Federation?
Answer:At Gateway level:At Subgraph level:
- Validate JWT in gateway middleware
- Add user info to context
- Forward auth headers to subgraphs
Copy
class AuthenticatedDataSource extends RemoteGraphQLDataSource {
willSendRequest({ request, context }) {
request.http.headers.set('authorization', context.token);
request.http.headers.set('x-user-id', context.user?.id);
}
}
- Read user from headers
- Apply authorization in resolvers
- Use directives for declarative auth
Chapter Summary
Key Takeaways:
- Federation composes unified GraphQL from multiple services
- Entities enable cross-subgraph type resolution
- Gateway handles query planning and execution
- Use @key, @external, @requires for entity relationships
- Forward authentication from gateway to subgraphs
- Test federated queries with mock subgraphs