GraphQL’s query model is well-understood, but subscriptions, federation, and schema design for large APIs have patterns that are easy to get wrong. Claude Code generates subscription resolvers with proper cleanup, federation subgraph configurations that compose correctly, and DataLoader setups that prevent N+1 problems — the three areas where GraphQL implementations most often have bugs.
This guide covers advanced GraphQL with Claude Code: subscriptions for real-time data, Apollo Federation for microservices, persisted queries, and performance optimization.
GraphQL Subscriptions
Add subscriptions to the order tracking feature.
When order status changes, connected clients should receive updates in real-time.
Don't use polling.
// schema
const typeDefs = gql`
type Subscription {
orderStatusChanged(orderId: ID!): OrderStatusUpdate!
}
type OrderStatusUpdate {
orderId: ID!
status: OrderStatus!
updatedAt: String!
estimatedDelivery: String
}
enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
CANCELLED
}
`;
// resolver — using PubSub for single-server, Redis PubSub for multi-server
import { PubSub, withFilter } from 'graphql-subscriptions';
import { RedisPubSub } from 'graphql-redis-subscriptions';
// Use Redis PubSub if running multiple server instances
const pubsub = process.env.REDIS_URL
? new RedisPubSub({
publisher: new Redis(process.env.REDIS_URL),
subscriber: new Redis(process.env.REDIS_URL),
})
: new PubSub();
const ORDER_STATUS_CHANGED = 'ORDER_STATUS_CHANGED';
const resolvers = {
Subscription: {
orderStatusChanged: {
// withFilter ensures clients only receive events for their specific order
subscribe: withFilter(
() => pubsub.asyncIterator(ORDER_STATUS_CHANGED),
(payload, variables) => {
return payload.orderStatusChanged.orderId === variables.orderId;
},
),
// Authorization — verify requester owns this order
resolve: async (payload, _args, context) => {
const order = await context.loaders.order.load(payload.orderStatusChanged.orderId);
if (order.userId !== context.userId) {
throw new ForbiddenError('Access denied');
}
return payload.orderStatusChanged;
},
},
},
Mutation: {
updateOrderStatus: async (_parent, { orderId, status }, context) => {
// Update in database
const order = await Order.findByIdAndUpdate(
orderId,
{ status, updatedAt: new Date() },
{ new: true },
);
// Publish to all subscribed clients
await pubsub.publish(ORDER_STATUS_CHANGED, {
orderStatusChanged: {
orderId: order.id,
status: order.status,
updatedAt: order.updatedAt.toISOString(),
estimatedDelivery: order.estimatedDelivery?.toISOString(),
},
});
return order;
},
},
};
WebSocket Transport Setup
Configure Apollo Server with both HTTP and WebSocket transport.
HTTP for queries/mutations, WebSockets for subscriptions.
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import express from 'express';
const app = express();
const httpServer = createServer(app);
// WebSocket server for subscriptions
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql',
});
const schema = makeExecutableSchema({ typeDefs, resolvers });
const serverCleanup = useServer(
{
schema,
context: async (ctx, msg, args) => {
// Extract auth token from WebSocket connection params
const token = ctx.connectionParams?.authorization as string;
const user = token ? await verifyToken(token) : null;
return { user, loaders: createLoaders() };
},
onDisconnect: (ctx) => {
console.log('Client disconnected', ctx.connectionParams?.clientId);
},
},
wsServer,
);
const server = new ApolloServer({
schema,
plugins: [
// Proper shutdown for HTTP
ApolloServerPluginDrainHttpServer({ httpServer }),
// Proper shutdown for WebSocket
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose();
},
};
},
},
],
});
await server.start();
app.use('/graphql', cors(), express.json(), expressMiddleware(server, {
context: async ({ req }) => ({
user: await getUserFromRequest(req),
loaders: createLoaders(),
}),
}));
httpServer.listen(4000, () => {
console.log('GraphQL server ready at http://localhost:4000/graphql');
console.log('Subscriptions ready at ws://localhost:4000/graphql');
});
Client-Side Subscription
// Frontend — Apollo Client with subscription
import { gql, useSubscription } from '@apollo/client';
const ORDER_STATUS_SUBSCRIPTION = gql`
subscription OnOrderStatusChanged($orderId: ID!) {
orderStatusChanged(orderId: $orderId) {
orderId
status
updatedAt
estimatedDelivery
}
}
`;
function OrderTracker({ orderId }: { orderId: string }) {
const { data, loading, error } = useSubscription(ORDER_STATUS_SUBSCRIPTION, {
variables: { orderId },
onData: ({ data }) => {
// Show toast notification for status change
toast.info(`Order status: ${data.data?.orderStatusChanged.status}`);
},
});
if (loading) return <p>Connecting to order tracking...</p>;
if (error) return <p>Connection error — retrying...</p>;
return (
<div>
<p>Status: {data?.orderStatusChanged.status}</p>
{data?.orderStatusChanged.estimatedDelivery && (
<p>Estimated delivery: {new Date(data.orderStatusChanged.estimatedDelivery).toLocaleDateString()}</p>
)}
</div>
);
}
Apollo Federation
We have separate User and Order microservices.
Each has its own GraphQL API. Federate them into a single schema.
User Subgraph
// user-service/schema.ts
import { buildSubgraphSchema } from '@apollo/subgraph';
const typeDefs = gql`
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@shareable"])
type User @key(fields: "id") {
id: ID!
email: String!
name: String!
createdAt: String!
}
type Query {
me: User
user(id: ID!): User
}
`;
const resolvers = {
User: {
// __resolveReference is called when another subgraph references a User
__resolveReference: async (reference: { id: string }) => {
return await UserModel.findById(reference.id);
},
},
Query: {
me: async (_parent, _args, context) => UserModel.findById(context.userId),
user: async (_parent, { id }) => UserModel.findById(id),
},
};
export const schema = buildSubgraphSchema({ typeDefs, resolvers });
Order Subgraph — Extending User
// order-service/schema.ts
const typeDefs = gql`
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@external"])
# Reference the User type from user-service — extend it with order data
type User @key(fields: "id") {
id: ID! @external
orders: [Order!]! # Added by this subgraph
}
type Order {
id: ID!
status: OrderStatus!
totalCents: Int!
createdAt: String!
user: User! # References User from another subgraph
}
type Query {
order(id: ID!): Order
}
`;
const resolvers = {
User: {
// Resolve User.orders when queried through the gateway
orders: async (user: { id: string }) => {
return OrderModel.find({ userId: user.id });
},
},
Order: {
user: (order) => ({ __typename: 'User', id: order.userId }),
},
};
Gateway Configuration
// gateway/index.ts
import { ApolloGateway, IntrospectAndCompose } from '@apollo/gateway';
import { ApolloServer } from '@apollo/server';
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'users', url: process.env.USER_SERVICE_URL ?? 'http://localhost:4001/graphql' },
{ name: 'orders', url: process.env.ORDER_SERVICE_URL ?? 'http://localhost:4002/graphql' },
{ name: 'products', url: process.env.PRODUCT_SERVICE_URL ?? 'http://localhost:4003/graphql' },
],
}),
});
const server = new ApolloServer({ gateway });
Now a client can query { me { name orders { status totalCents } } } through the gateway — User resolves from the user service, orders from the order service, composed automatically.
Persisted Queries
Our mobile app sends large query strings on every request.
Set up persisted queries to reduce payload size.
// Generate the persisted query manifest at build time
// apollo-codegen generates a hash map of {hash: queryString}
// Apollo Server setup
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash';
// Client — send hash first, then fall back to full query on 404
const persistedQueriesLink = createPersistedQueryLink({
sha256,
useGETForHashedQueries: true, // GET requests for hashed queries (better caching)
});
// Server — use persisted query store
import { KeyValueCache } from '@apollo/utils.keyvaluecache';
const server = new ApolloServer({
schema,
plugins: [
responseCachePlugin(), // Cache query results
{
requestDidStart: async () => ({
async parsingDidStart({ queryString, request }) {
// Reject full query strings in production — only allow known hashes
if (process.env.NODE_ENV === 'production' && !request.extensions?.persistedQuery) {
throw new ForbiddenError('Only persisted queries are allowed');
}
},
}),
},
],
});
After the first request, mobile clients send only a 64-character hash instead of the full query string. Combined with GET requests, these are CDN-cacheable.
N+1 Optimization
The query { orders { user { name } } } is making one database
query per order to fetch the user. Fix it.
DataLoader batches and caches within a single request:
import DataLoader from 'dataloader';
// Create per-request (NOT global — would mix data between requests)
function createLoaders() {
return {
user: new DataLoader<string, User>(async (userIds) => {
// Single DB query for all user IDs in this batch
const users = await UserModel.find({ _id: { $in: userIds } });
const userMap = new Map(users.map(u => [u.id, u]));
// Must return in same order as input IDs
return userIds.map(id => userMap.get(id) ?? new Error(`User ${id} not found`));
}),
ordersByUser: new DataLoader<string, Order[]>(async (userIds) => {
const orders = await OrderModel.find({ userId: { $in: userIds } });
const ordersByUser = new Map<string, Order[]>();
for (const order of orders) {
const existing = ordersByUser.get(order.userId) ?? [];
ordersByUser.set(order.userId, [...existing, order]);
}
return userIds.map(id => ordersByUser.get(id) ?? []);
}),
};
}
// Resolver uses the loader
const resolvers = {
Order: {
user: (order, _args, context) => context.loaders.user.load(order.userId),
},
User: {
orders: (user, _args, context) => context.loaders.ordersByUser.load(user.id),
},
};
Now { orders { user { name } } } with 100 orders makes 2 DB queries total: one for orders, one batched query for all unique user IDs.
For the foundational GraphQL setup including schema design and Apollo Server 4 context, see the GraphQL guide. For deploying GraphQL services as microservices with Kubernetes and Docker, see the Kubernetes guide and microservices guide. The Claude Skills 360 bundle includes advanced GraphQL skill sets covering federation patterns and real-time architecture. Start with the free tier to try GraphQL schema generation and optimization patterns.