WebSockets & Real-Time Apps

Real-Time with GraphQL Subscriptions

17 min Lesson 24 of 35

Introduction to GraphQL Subscriptions

GraphQL subscriptions provide a way to push real-time updates from the server to clients when specific events occur. Unlike queries (request data) and mutations (modify data), subscriptions maintain an active connection and stream updates.

What are GraphQL Subscriptions?

Subscriptions are a GraphQL feature that allows:

  • Real-time Updates: Push data to clients as events happen
  • Event-Driven: Clients subscribe to specific events
  • WebSocket-Based: Uses WebSocket protocol for bidirectional communication
  • Type-Safe: Full GraphQL type system support
Key Concept: Subscriptions complete the GraphQL triad: Queries (read), Mutations (write), Subscriptions (real-time updates).

Why Use GraphQL Subscriptions?

Benefits over traditional REST WebSockets:

  • Type-safe real-time data with GraphQL schema
  • Request exactly the data you need (no over-fetching)
  • Built-in filtering and arguments
  • Unified API surface (queries, mutations, subscriptions in one endpoint)
  • Strong tooling and client support (Apollo, Relay, URQL)

GraphQL Subscription Schema

Define subscriptions in your GraphQL schema:

// schema.graphql type Message { id: ID! content: String! author: User! createdAt: String! } type User { id: ID! username: String! avatar: String } type Query { messages: [Message!]! } type Mutation { sendMessage(content: String!): Message! } type Subscription { # Subscribe to new messages in a room messageAdded(roomId: ID!): Message! # Subscribe to typing indicators userTyping(roomId: ID!): User! # Subscribe to user status changes userStatusChanged(userId: ID!): UserStatus! } enum UserStatus { ONLINE OFFLINE AWAY }

Apollo Server Setup

Set up Apollo Server with subscription support:

// Install dependencies // npm install apollo-server-express graphql subscriptions-transport-ws const { ApolloServer, gql } = require('apollo-server-express'); const { PubSub } = require('graphql-subscriptions'); const { createServer } = require('http'); const express = require('express'); const { ApolloServerPluginDrainHttpServer } = require('apollo-server-core'); const { makeExecutableSchema } = require('@graphql-tools/schema'); const { WebSocketServer } = require('ws'); const { useServer } = require('graphql-ws/lib/use/ws'); const app = express(); const httpServer = createServer(app); // PubSub instance for publishing/subscribing to events const pubsub = new PubSub(); // GraphQL schema const typeDefs = gql` type Message { id: ID! content: String! author: String! timestamp: String! } type Query { messages: [Message!]! } type Mutation { sendMessage(content: String!, author: String!): Message! } type Subscription { messageAdded: Message! } `; // Resolvers const resolvers = { Query: { messages: () => messages }, Mutation: { sendMessage: (_, { content, author }) => { const message = { id: String(Date.now()), content, author, timestamp: new Date().toISOString() }; messages.push(message); // Publish event to subscribers pubsub.publish('MESSAGE_ADDED', { messageAdded: message }); return message; } }, Subscription: { messageAdded: { subscribe: () => pubsub.asyncIterator(['MESSAGE_ADDED']) } } }; // Create schema const schema = makeExecutableSchema({ typeDefs, resolvers }); // WebSocket server for subscriptions const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql' }); const serverCleanup = useServer({ schema }, wsServer); // Apollo Server const server = new ApolloServer({ schema, plugins: [ ApolloServerPluginDrainHttpServer({ httpServer }), { async serverWillStart() { return { async drainServer() { await serverCleanup.dispose(); } }; } } ] }); await server.start(); server.applyMiddleware({ app }); httpServer.listen(4000, () => { console.log(`Server ready at http://localhost:4000${server.graphqlPath}`); console.log(`Subscriptions ready at ws://localhost:4000${server.graphqlPath}`); });
Important: Subscriptions require both HTTP server (for queries/mutations) and WebSocket server (for subscriptions) running on the same port.

Subscription Resolvers

Subscription resolvers use async iterators:

const resolvers = { Subscription: { messageAdded: { // Simple subscription subscribe: () => pubsub.asyncIterator(['MESSAGE_ADDED']) }, // Subscription with filtering messageAddedInRoom: { subscribe: (_, { roomId }) => { return pubsub.asyncIterator([`MESSAGE_ADDED_${roomId}`]); } }, // Subscription with custom filtering messageAddedForUser: { subscribe: () => pubsub.asyncIterator(['MESSAGE_ADDED']), resolve: (payload, args, context) => { // Filter messages based on user permissions if (payload.messageAdded.private && payload.messageAdded.recipientId !== context.userId) { return null; // Don't send to this subscriber } return payload.messageAdded; } } } }; // Publishing events const sendMessage = async (roomId, message) => { await db.messages.create(message); // Publish to room-specific channel pubsub.publish(`MESSAGE_ADDED_${roomId}`, { messageAddedInRoom: message }); };

PubSub for Event Publishing

The PubSub pattern connects mutations to subscriptions:

const { PubSub } = require('graphql-subscriptions'); const pubsub = new PubSub(); // Event names as constants const EVENTS = { MESSAGE_ADDED: 'MESSAGE_ADDED', USER_TYPING: 'USER_TYPING', USER_STATUS_CHANGED: 'USER_STATUS_CHANGED' }; const resolvers = { Mutation: { sendMessage: async (_, { roomId, content }, { userId }) => { const message = await db.messages.create({ roomId, content, authorId: userId }); // Publish to all subscribers await pubsub.publish(EVENTS.MESSAGE_ADDED, { messageAdded: message, roomId // Can be used for filtering }); return message; }, setTyping: async (_, { roomId, isTyping }, { userId }) => { await pubsub.publish(EVENTS.USER_TYPING, { userTyping: { userId, roomId, isTyping } }); return { success: true }; } }, Subscription: { messageAdded: { subscribe: (_, { roomId }) => { return pubsub.asyncIterator([EVENTS.MESSAGE_ADDED]); }, resolve: (payload, { roomId }) => { // Filter by room if (payload.roomId === roomId) { return payload.messageAdded; } return null; } }, userTyping: { subscribe: (_, { roomId }) => { return pubsub.asyncIterator([EVENTS.USER_TYPING]); }, resolve: (payload, { roomId }) => { if (payload.userTyping.roomId === roomId) { return payload.userTyping; } return null; } } } };
Best Practice: Use constants for event names to avoid typos and make refactoring easier.

Filtering Subscriptions

Filter which subscribers receive events:

// Method 1: Channel-based filtering (most efficient) const resolvers = { Subscription: { messageAdded: { subscribe: (_, { roomId }) => { // Each room has its own channel return pubsub.asyncIterator([`ROOM_${roomId}_MESSAGES`]); } } }, Mutation: { sendMessage: async (_, { roomId, content }) => { const message = await saveMessage(roomId, content); // Publish to room-specific channel pubsub.publish(`ROOM_${roomId}_MESSAGES`, { messageAdded: message }); return message; } } }; // Method 2: Resolver-based filtering (more flexible) const resolvers = { Subscription: { messageAdded: { subscribe: () => pubsub.asyncIterator(['ALL_MESSAGES']), resolve: (payload, args, context) => { const { messageAdded } = payload; const { roomId } = args; const { user } = context; // Only send if user is in the room if (messageAdded.roomId !== roomId) return null; // Only send if user has permission if (!user.hasAccessToRoom(roomId)) return null; return messageAdded; } } } }; // Method 3: withFilter helper (Apollo provides this) const { withFilter } = require('graphql-subscriptions'); const resolvers = { Subscription: { messageAdded: { subscribe: withFilter( () => pubsub.asyncIterator(['MESSAGE_ADDED']), (payload, variables, context) => { // Return true to send, false to skip return payload.messageAdded.roomId === variables.roomId && context.user.hasAccessToRoom(variables.roomId); } ) } } };

Client-Side Subscription Handling

Connect and subscribe from a React client:

// Install dependencies // npm install @apollo/client graphql subscriptions-transport-ws import { ApolloClient, InMemoryCache, HttpLink, split } from '@apollo/client'; import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; import { getMainDefinition } from '@apollo/client/utilities'; import { createClient } from 'graphql-ws'; // HTTP connection for queries and mutations const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql' }); // WebSocket connection for subscriptions const wsLink = new GraphQLWsLink(createClient({ url: 'ws://localhost:4000/graphql' })); // Split traffic between HTTP and WebSocket const splitLink = split( ({ query }) => { const definition = getMainDefinition(query); return ( definition.kind === 'OperationDefinition' && definition.operation === 'subscription' ); }, wsLink, httpLink ); // Apollo Client const client = new ApolloClient({ link: splitLink, cache: new InMemoryCache() });

Using Subscriptions in React

import { useSubscription, gql, useMutation } from '@apollo/client'; const MESSAGE_ADDED = gql` subscription MessageAdded($roomId: ID!) { messageAdded(roomId: $roomId) { id content author { id username avatar } createdAt } } `; const SEND_MESSAGE = gql` mutation SendMessage($roomId: ID!, $content: String!) { sendMessage(roomId: $roomId, content: $content) { id content author { id username } createdAt } } `; function ChatRoom({ roomId }) { const [messages, setMessages] = useState([]); // Subscribe to new messages const { data, loading, error } = useSubscription(MESSAGE_ADDED, { variables: { roomId }, onSubscriptionData: ({ subscriptionData }) => { const newMessage = subscriptionData.data.messageAdded; setMessages(prev => [...prev, newMessage]); } }); // Mutation to send messages const [sendMessage] = useMutation(SEND_MESSAGE); const handleSend = async (content) => { await sendMessage({ variables: { roomId, content } }); }; return ( <div> {messages.map(msg => ( <div key={msg.id}> <strong>{msg.author.username}:</strong> {msg.content} </div> ))} <MessageInput onSend={handleSend} /> </div> ); }
Exercise:
  1. Set up an Apollo Server with GraphQL subscriptions
  2. Create a schema with:
    • Query for fetching initial data
    • Mutation for creating posts
    • Subscription for receiving new posts
  3. Implement filtering so users only receive posts from followed users
  4. Create a React client with Apollo Client
  5. Subscribe to real-time updates and display them in the UI
  6. Test with multiple browser windows to verify real-time sync

Summary

GraphQL subscriptions provide type-safe real-time communication:

  • WebSocket-based bidirectional communication
  • Type-safe with full GraphQL schema support
  • Request exactly the data you need (no over-fetching)
  • Built-in filtering with arguments and resolvers
  • Unified API with queries and mutations
  • Strong client support with Apollo, Relay, URQL