DEV Community

Atlas Whoff
Atlas Whoff

Posted on

GraphQL Subscriptions: Real-Time Data With Type Safety

When GraphQL Subscriptions Make Sense

GraphQL has three operation types:

  • query — fetch data
  • mutation — change data
  • subscription — receive updates in real-time

Subscriptions make sense when you already have a GraphQL API and need real-time features without adding a separate WebSocket layer.

Server Setup with GraphQL Yoga

npm install graphql-yoga graphql
Enter fullscreen mode Exit fullscreen mode
import { createYoga, createSchema } from 'graphql-yoga';
import { createPubSub } from '@graphql-yoga/subscription';
import { createServer } from 'http';

// Type-safe pub/sub
const pubSub = createPubSub<{
  MESSAGE_CREATED: [channelId: string, payload: { message: Message }];
  USER_TYPING: [channelId: string, payload: { userId: string }];
}>();

const schema = createSchema({
  typeDefs: /* GraphQL */ `
    type Message {
      id: ID!
      content: String!
      userId: String!
      createdAt: String!
    }

    type Query {
      messages(channelId: ID!): [Message!]!
    }

    type Mutation {
      sendMessage(channelId: ID!, content: String!): Message!
    }

    type Subscription {
      messageCreated(channelId: ID!): Message!
      userTyping(channelId: ID!): String!
    }
  `,

  resolvers: {
    Query: {
      messages: (_, { channelId }) => db.messages.findMany({ where: { channelId } }),
    },

    Mutation: {
      sendMessage: async (_, { channelId, content }, { userId }) => {
        const message = await db.messages.create({
          data: { channelId, content, userId },
        });

        // Publish to subscribers
        pubSub.publish('MESSAGE_CREATED', channelId, { message });

        return message;
      },
    },

    Subscription: {
      messageCreated: {
        // Return async iterator
        subscribe: (_, { channelId }) =>
          pubSub.subscribe('MESSAGE_CREATED', channelId),

        // Transform the event payload
        resolve: (payload) => payload.message,
      },

      userTyping: {
        subscribe: (_, { channelId }) =>
          pubSub.subscribe('USER_TYPING', channelId),
        resolve: (payload) => payload.userId,
      },
    },
  },
});

const yoga = createYoga({ schema });
const server = createServer(yoga);
server.listen(4000);
Enter fullscreen mode Exit fullscreen mode

Client with Apollo

import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { getMainDefinition } from '@apollo/client/utilities';

// HTTP for queries/mutations
const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql' });

// WebSocket for subscriptions
const wsLink = new GraphQLWsLink(createClient({
  url: 'ws://localhost:4000/graphql',
}));

// Route by operation type
const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return definition.kind === 'OperationDefinition' && definition.operation === 'subscription';
  },
  wsLink,
  httpLink,
);

const client = new ApolloClient({ link: splitLink, cache: new InMemoryCache() });
Enter fullscreen mode Exit fullscreen mode
// React component
import { useSubscription, gql } from '@apollo/client';

const MESSAGE_SUBSCRIPTION = gql`
  subscription OnMessageCreated($channelId: ID!) {
    messageCreated(channelId: $channelId) {
      id
      content
      userId
      createdAt
    }
  }
`;

function MessageList({ channelId }: { channelId: string }) {
  const [messages, setMessages] = useState<Message[]>([]);

  useSubscription(MESSAGE_SUBSCRIPTION, {
    variables: { channelId },
    onData: ({ data }) => {
      if (data.data?.messageCreated) {
        setMessages(prev => [...prev, data.data.messageCreated]);
      }
    },
  });

  return (
    <ul>
      {messages.map(msg => (
        <li key={msg.id}>{msg.content}</li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

Scaling Subscriptions

In-process pub/sub doesn't work with multiple server instances:

import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';

const pubSub = new RedisPubSub({
  publisher: new Redis(process.env.REDIS_URL),
  subscriber: new Redis(process.env.REDIS_URL),
});

// Now subscriptions work across all server instances
pubSub.publish('MESSAGE_CREATED', { channelId, message });
Enter fullscreen mode Exit fullscreen mode

When NOT to Use GraphQL Subscriptions

  • Simple use case: SSE is easier and lighter
  • Non-GraphQL API: Don't add GraphQL just for subscriptions
  • Very high frequency updates: WebSocket with binary protocol (like Socket.io rooms) is more efficient

If you're already on GraphQL and need real-time: subscriptions are the right tool. If you're starting fresh: evaluate SSE first.


GraphQL API with queries, mutations, and subscriptions: Whoff Agents AI SaaS Starter Kit includes both REST and GraphQL options.

Top comments (0)