Skip to main content

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?

┌─────────────────────────────────────────────────────────────────────────────┐
│                    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

┌─────────────────────────────────────────────────────────────────────────────┐
│                    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

// 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

// 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

// 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

// 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)

# 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

# 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!
}
// 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

# Entity with compound key
type CartItem @key(fields: "userId productId") {
  userId: ID!
  productId: ID!
  quantity: Int!
  addedAt: String!
}

Interface Entities

# 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

// 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

// 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

// 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

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
How it works:
  1. Each subgraph defines its portion of the schema
  2. Gateway fetches schemas and composes supergraph
  3. Client sends query to gateway
  4. Gateway plans execution across subgraphs
  5. Results are combined and returned
Key directives:
  • @key: Defines entity’s unique identifier
  • @external: Field defined in another subgraph
  • @requires: Field needs external data
Answer:Entity: A type that can be resolved across multiple subgraphs.Definition:
type User @key(fields: "id") {
  id: ID!
  name: String!
}
Reference resolution:
User: {
  __resolveReference: (userRef) => {
    // userRef = { __typename: 'User', id: '123' }
    return findUserById(userRef.id);
  }
}
How references work:
  1. Orders subgraph returns { __typename: 'User', id: '1' }
  2. Gateway sees it needs User fields
  3. Gateway calls Users subgraph’s __resolveReference
  4. Full User data is returned
Answer:@external: Marks a field as defined in another subgraph.
type Product @key(fields: "id") {
  id: ID! @external
  name: String! @external
}
@requires: Indicates a field needs external data to resolve.
type Product @key(fields: "id") {
  id: ID! @external
  price: Float! @external
  
  # This field needs 'price' from Products subgraph
  priceWithTax: Float! @requires(fields: "price")
}
Execution:
  1. Gateway fetches price from Products subgraph
  2. Passes it to this subgraph’s resolver
  3. Resolver uses it to compute priceWithTax
Answer:At Gateway level:
  1. Validate JWT in gateway middleware
  2. Add user info to context
  3. Forward auth headers to subgraphs
class AuthenticatedDataSource extends RemoteGraphQLDataSource {
  willSendRequest({ request, context }) {
    request.http.headers.set('authorization', context.token);
    request.http.headers.set('x-user-id', context.user?.id);
  }
}
At Subgraph level:
  • Read user from headers
  • Apply authorization in resolvers
  • Use directives for declarative auth
Best practice: Centralize authentication at gateway, distribute authorization to subgraphs.

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
Next Chapter: Interview Preparation - Comprehensive practice for microservices interviews.