Claude Code for Real-Time GraphQL: Subscriptions, Federation, and Performance — Claude Skills 360 Blog
Blog / Development / Claude Code for Real-Time GraphQL: Subscriptions, Federation, and Performance
Development

Claude Code for Real-Time GraphQL: Subscriptions, Federation, and Performance

Published: May 31, 2026
Read time: 9 min read
By: Claude Skills 360

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.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free