Apollo Federation 2 composes multiple GraphQL subgraphs into a single unified supergraph. Each microservice owns its slice of the schema, declares entity keys, and extends types owned by other services. The Apollo Router (Rust-based) routes queries to subgraphs based on the query plan. Claude Code designs subgraph schemas with @key, @external, @requires, and @provides directives, implements entity resolvers, configures the Apollo Router, and writes the federation integration tests.
CLAUDE.md for Federation Projects
## Federation Stack
- Apollo Federation 2.7 (spec 2.4+)
- Router: Apollo Router 1.x (Rust-based, replaces Apollo Gateway)
- Subgraph server: Apollo Server 4 + @apollo/subgraph
- Schema composition: rover CLI (local) + GraphOS Studio (CI)
- Subgraph services: orders-subgraph, users-subgraph, inventory-subgraph, payments-subgraph
- Client: Apollo Client 3 or URQL v4
- Testing: jest + @apollo/subgraph-testing-utils for subgraph unit tests
- Each subgraph owns its primary entities — no cross-subgraph extends for primary keys
Subgraph 1: Users
# users-subgraph/schema.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.4", import: ["@key", "@shareable"])
type Query {
user(id: ID!): User
me: User
}
type User @key(fields: "id") {
id: ID!
email: String!
name: String!
# avatarUrl is expensive — only resolve if explicitly selected
avatarUrl: String @external
createdAt: DateTime!
}
# Value type: shared by all subgraphs (no @key — not an entity)
type Address @shareable {
street: String!
city: String!
country: String!
postalCode: String!
}
scalar DateTime
// users-subgraph/src/resolvers.ts
export const resolvers = {
Query: {
user: async (_, { id }, { dataSources }) =>
dataSources.usersAPI.getUser(id),
me: async (_, __, { user }) => user,
},
User: {
// Entity resolver — called when another subgraph references User by key
__resolveReference: async ({ id }, { dataSources }) =>
dataSources.usersAPI.getUser(id),
},
};
Subgraph 2: Orders
# orders-subgraph/schema.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.4",
import: ["@key", "@external", "@requires", "@provides"])
type Query {
order(id: ID!): Order
ordersByUser(userId: ID!, first: Int = 20, after: String): OrderConnection!
}
type Mutation {
createOrder(input: CreateOrderInput!): CreateOrderPayload!
cancelOrder(id: ID!, reason: String): Order!
}
type Order @key(fields: "id") {
id: ID!
# user field: stub — actual User data resolved by users-subgraph
user: User!
status: OrderStatus!
items: [OrderItem!]!
totalCents: Int!
createdAt: DateTime!
updatedAt: DateTime!
}
type OrderItem {
product: Product!
quantity: Int!
unitPriceCents: Int!
}
# Stub for User entity — orders-subgraph knows userId internally
type User @key(fields: "id", resolvable: false) {
id: ID!
}
# Stub for Product — provided by inventory-subgraph
type Product @key(fields: "id") {
id: ID!
# @requires: fetch these fields from inventory-subgraph before resolving subtotal
price: Int @external
# @provides: order items can tell the router that product.name is pre-populated
name: String @external
}
enum OrderStatus {
PENDING
PROCESSING
SHIPPED
DELIVERED
CANCELLED
}
type OrderConnection {
edges: [OrderEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type OrderEdge {
cursor: String!
node: Order!
}
input CreateOrderInput {
userId: ID!
items: [OrderItemInput!]!
}
type CreateOrderPayload {
order: Order
userErrors: [UserError!]!
}
// orders-subgraph/src/resolvers.ts
export const resolvers = {
Query: {
order: async (_, { id }, { db }) => db.orders.findById(id),
ordersByUser: async (_, { userId, first, after }, { db }) =>
db.orders.paginatedByUser(userId, first, after),
},
Mutation: {
createOrder: async (_, { input }, { db, pubsub }) => {
const order = await db.orders.create(input);
pubsub.publish('ORDER_CREATED', { orderCreated: order });
return { order, userErrors: [] };
},
},
Order: {
// Entity resolver: called when orders-subgraph is asked to resolve an Order by id
__resolveReference: async ({ id }, { db }) => db.orders.findById(id),
// Return stub object — users-subgraph resolves the full User
user: (order) => ({ __typename: 'User', id: order.userId }),
// items with product stubs — inventory-subgraph resolves full Product
items: async (order, _, { db }) => {
const items = await db.orderItems.byOrder(order.id);
return items.map(item => ({
...item,
product: { __typename: 'Product', id: item.productId },
}));
},
},
};
Subgraph 3: Inventory contributing to Order
# inventory-subgraph/schema.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.4",
import: ["@key", "@external", "@requires"])
type Query {
product(id: ID!): Product
products(ids: [ID!]!): [Product!]!
}
type Product @key(fields: "id") {
id: ID!
name: String!
description: String
price: Int! # cents
stockCount: Int!
sku: String!
}
Apollo Router Configuration
# router.yaml
supergraph:
path: /graphql
sandbox:
enabled: false # Disable in production
cors:
origins:
- https://myapp.com
- https://admin.myapp.com
headers:
all:
request:
- propagate:
matching: ^x-
- insert:
name: x-router-id
value: apollo-router
authentication:
router:
jwt:
jwks:
- url: https://auth.myapp.com/.well-known/jwks.json
authorization:
directives:
enabled: true
# Demand control: prevent expensive queries
preview_demand_control:
enabled: true
mode: enforce
strategy:
cost_per_field: 1
default_budget: 1000
# Per-subgraph circuit breaking
traffic_shaping:
all:
deduplicate_query: true
experimental_http2: negotiated
timeout: 30s
subgraphs:
users:
timeout: 5s
orders:
timeout: 10s
inventory:
timeout: 5s
experimental_retry:
min_per_sec: 10
ttl: 10s
retry_mutations: false
telemetry:
exporters:
tracing:
otlp:
endpoint: http://otel-collector:4318
protocol: http
instrumentation:
spans:
router:
attributes:
graphql.operation.type: true
graphql.operation.name: true
Federation Integration Test
// tests/federation.test.ts
import { buildSubgraphSchema } from '@apollo/subgraph';
import { ApolloServer } from '@apollo/server';
import { addMocksToSchema } from '@graphql-tools/mock';
async function createTestSubgraph(typeDefs: DocumentNode, resolvers: object) {
const schema = buildSubgraphSchema({ typeDefs, resolvers });
const server = new ApolloServer({ schema });
await server.start();
return server;
}
it('resolves User entity from orders-subgraph', async () => {
const { ordersServer } = await setup();
const response = await ordersServer.executeOperation({
query: `
query ($representations: [_Any!]!) {
_entities(representations: $representations) {
... on Order {
id
status
user { id }
}
}
}
`,
variables: {
representations: [{ __typename: 'Order', id: 'ord_123' }],
},
});
expect(response.body.kind).toBe('single');
expect(response.body.singleResult.errors).toBeUndefined();
const order = response.body.singleResult.data!._entities[0];
expect(order.user.__typename).toBe('User');
});
For the underlying GraphQL server patterns before adding federation, see the GraphQL guide for resolver design and schema-first development. For the subscription handling in federation where the router passes real-time events, the WebSocket scaling guide covers pub/sub infrastructure. The Claude Skills 360 bundle includes GraphQL Federation skill sets covering subgraph design, entity resolution, and Apollo Router configuration. Start with the free tier to try federation subgraph generation.