GraphQL Federation composes multiple GraphQL services into a single supergraph. Each team owns a subgraph — a GraphQL service that handles their domain’s types and queries. The Apollo Router (or gateway) merges them into one schema that clients query. Federation solves the problem of teams wanting independent GraphQL APIs that still appear unified to the client. Claude Code writes subgraph schemas, implements entity resolvers, and configures the router.
CLAUDE.md for GraphQL Federation Projects
## GraphQL Federation Stack
- Federation: Apollo Federation v2
- Router: Apollo Router (Rust-based, replace JS gateway for production)
- Subgraph framework: Apollo Server 4 (Node.js) or Hot Chocolate (C#/.NET)
- Schema management: Apollo Schema Registry (GraphOS)
- Auth: JWT validated at router level, user context forwarded to subgraphs
- Query planning: enabled by default in Apollo Router
- Testing: @apollo/subgraph + jest for unit; rover subgraph check for CI
Subgraph: Orders Service
// orders-service/src/schema.ts
import { buildSubgraphSchema } from '@apollo/subgraph';
import gql from 'graphql-tag';
export const typeDefs = gql`
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@external", "@requires", "@provides", "@shareable"])
# This subgraph owns the Order type
type Order @key(fields: "id") {
id: ID!
customerId: ID!
status: OrderStatus!
totalCents: Int!
items: [OrderItem!]!
createdAt: String!
# This service provides the Customer via the customers subgraph's @key
customer: Customer!
}
type OrderItem {
productId: ID!
quantity: Int!
unitPriceCents: Int!
# Product comes from the products subgraph
product: Product!
}
# Stub types for entities owned by other subgraphs
type Customer @key(fields: "id", resolvable: false) {
id: ID!
}
type Product @key(fields: "id", resolvable: false) {
id: ID!
}
enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
CANCELLED
}
type Query {
order(id: ID!): Order
orders(customerId: ID!, first: Int = 20, after: String): OrderConnection!
}
type Mutation {
placeOrder(input: PlaceOrderInput!): PlaceOrderPayload!
cancelOrder(orderId: ID!, reason: String!): CancelOrderPayload!
}
# ... Connection/Input/Payload types ...
`;
// orders-service/src/resolvers.ts
export const resolvers = {
Query: {
order: async (_: any, { id }: { id: string }, ctx: Context) => {
return ctx.db.orders.findById(id);
},
orders: async (_: any, { customerId, first, after }: any, ctx: Context) => {
return ctx.db.orders.findByCustomer(customerId, { first, after });
},
},
Mutation: {
placeOrder: async (_: any, { input }: any, ctx: Context) => {
if (!ctx.userId) throw new GraphQLError('Not authenticated', {
extensions: { code: 'UNAUTHENTICATED' },
});
const order = await placeOrderService(ctx.db, input, ctx.userId);
return { order, success: true };
},
},
Order: {
// Entity resolver: when other subgraphs ask for Order by id
__resolveReference: async ({ id }: { id: string }, ctx: Context) => {
return ctx.db.orders.findById(id);
},
// Resolve the customer (foreign key → entity reference for customers subgraph)
customer: (order: Order) => {
// Return a stub with just the key — router fetches the rest from customers subgraph
return { __typename: 'Customer', id: order.customerId };
},
// Resolve order items
items: async (order: Order, _: any, ctx: Context) => {
return ctx.db.orderItems.findByOrderId(order.id);
},
},
OrderItem: {
product: (item: OrderItem) => ({
__typename: 'Product',
id: item.productId,
}),
},
};
Subgraph: Customers Service
// customers-service/src/schema.ts — owns Customer type
export const typeDefs = gql`
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"])
type Customer @key(fields: "id") {
id: ID!
email: String!
name: String!
plan: CustomerPlan!
# Computed by this subgraph when orders subgraph references a Customer
# @external: declared here, provided by orders subgraph
# @requires: need orderCount to compute loyaltyTier
orderCount: Int @external
loyaltyTier: LoyaltyTier @requires(fields: "orderCount")
}
enum CustomerPlan { FREE PRO ENTERPRISE }
enum LoyaltyTier { BRONZE SILVER GOLD PLATINUM }
type Query {
customer(id: ID!): Customer
me: Customer
}
`;
export const resolvers = {
Customer: {
__resolveReference: async ({ id }: { id: string }, ctx: Context) => {
return ctx.db.customers.findById(id);
},
// loyaltyTier uses the orderCount provided by orders subgraph via @requires
loyaltyTier: (customer: any) => {
const count = customer.orderCount ?? 0;
if (count >= 100) return 'PLATINUM';
if (count >= 50) return 'GOLD';
if (count >= 10) return 'SILVER';
return 'BRONZE';
},
},
Query: {
customer: (_: any, { id }: any, ctx: Context) => ctx.db.customers.findById(id),
me: (_: any, __: any, ctx: Context) => {
if (!ctx.userId) throw new GraphQLError('Not authenticated');
return ctx.db.customers.findById(ctx.userId);
},
},
};
Apollo Router Configuration
# router.yaml
supergraph:
listen: 0.0.0.0:4000
# Supergraph schema from Apollo GraphOS (managed federation)
# Or use rover supergraph compose for self-managed
headers:
all:
request:
- propagate:
matching: x-request-id # Forward request IDs to subgraphs
subgraphs:
orders:
request:
- insert:
name: "x-internal-service"
value: "router"
# Auth: validate JWT at router, forward user ID to subgraphs
authentication:
router:
jwt:
jwks:
- url: https://auth.myapp.com/.well-known/jwks.json
authorization:
require_authentication: false # Set true to require auth globally
# Coprocessor: custom auth/rate limiting
# coprocessor:
# url: http://auth-service:9001
# Performance
traffic_shaping:
router:
timeout: 30s
all:
request:
timeout: 25s
deduplicate_query: true # Dedup identical concurrent queries
# Telemetry
telemetry:
exporters:
tracing:
otlp:
endpoint: http://otel-collector:4317
metrics:
prometheus:
enabled: true
listen: 0.0.0.0:9090
path: /metrics
Schema CI with Rover
# .github/workflows/schema-check.yml
- name: Check Orders Subgraph Schema
run: |
rover subgraph check myorg@production \
--name orders \
--schema ./orders-service/schema.graphql \
--watch-delay 2
env:
APOLLO_KEY: ${{ secrets.APOLLO_KEY }}
- name: Publish Subgraph Schema
if: github.ref == 'refs/heads/main'
run: |
rover subgraph publish myorg@production \
--name orders \
--schema ./orders-service/schema.graphql \
--routing-url https://orders.internal.myapp.com/graphql
env:
APOLLO_KEY: ${{ secrets.APOLLO_KEY }}
@defer for Streaming
// Client: use @defer for non-critical fields
const CUSTOMER_WITH_ORDERS = gql`
query CustomerDashboard($id: ID!) {
customer(id: $id) {
id
name
email
# Defer the expensive order history
... on Customer @defer {
recentOrders {
id
status
totalCents
}
loyaltyTier
}
}
}
`;
// React: handle incremental delivery
function CustomerDashboard({ customerId }) {
const { data, loading } = useQuery(CUSTOMER_WITH_ORDERS, {
variables: { id: customerId },
});
return (
<div>
{/* Renders immediately when available */}
<h1>{data?.customer.name}</h1>
<p>{data?.customer.email}</p>
{/* Renders when deferred chunk arrives */}
{data?.customer.recentOrders ? (
<OrderList orders={data.customer.recentOrders} />
) : (
<OrderListSkeleton />
)}
</div>
);
}
For the REST APIs that often coexist with GraphQL Federation in larger systems, see the Go microservices guide for HTTP service patterns. For the authentication layer that the Apollo Router integrates with, the OAuth2 guide covers JWT validation. The Claude Skills 360 bundle includes GraphQL skill sets covering Federation v2 subgraphs, entity resolution, and router configuration. Start with the free tier to try Federation schema generation.