Skip to main content

Chapter 7: Subscriptions

GraphQL subscriptions enable real-time data updates in your application. In this chapter, we'll explore how to implement and use subscriptions with GraphQL Yoga.

WebSocket Setup

Basic Setup

First, let's set up WebSocket support in our GraphQL server:

import { createYoga } from '@graphql-yoga/node'
import { createServer } from 'node:http'
import { WebSocketServer } from 'ws'

const yoga = createYoga({
schema,
plugins: [
useGraphQLSSE() // Enable Server-Sent Events
]
})

const server = createServer(yoga)
const wss = new WebSocketServer({
server,
path: '/graphql'
})

server.listen(4000, () => {
console.log('Server is running on http://localhost:4000/graphql')
})

Client Setup

On the client side, you can use the graphql-ws client:

import { createClient } from 'graphql-ws'
import WebSocket from 'ws'

const client = createClient({
url: 'ws://localhost:4000/graphql',
webSocketImpl: WebSocket
})

Subscription Types

Defining Subscription Types

Add subscription types to your schema:

type Subscription {
postCreated: Post!
postUpdated: Post!
postDeleted: ID!
commentAdded(postId: ID!): Comment!
}

type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
}

type Comment {
id: ID!
content: String!
author: User!
post: Post!
}

Implementing Subscription Resolvers

import { PubSub } from 'graphql-subscriptions'

const pubsub = new PubSub()

const resolvers = {
Subscription: {
postCreated: {
subscribe: () => pubsub.asyncIterator(['POST_CREATED'])
},
postUpdated: {
subscribe: () => pubsub.asyncIterator(['POST_UPDATED'])
},
postDeleted: {
subscribe: () => pubsub.asyncIterator(['POST_DELETED'])
},
commentAdded: {
subscribe: (_, { postId }) =>
pubsub.asyncIterator([`COMMENT_ADDED_${postId}`])
}
},

Mutation: {
createPost: async (_, { input }, context) => {
const post = await context.prisma.post.create({
data: input
})

pubsub.publish('POST_CREATED', {
postCreated: post
})

return post
},

addComment: async (_, { postId, content }, context) => {
const comment = await context.prisma.comment.create({
data: {
content,
postId,
authorId: context.user.id
}
})

pubsub.publish(`COMMENT_ADDED_${postId}`, {
commentAdded: comment
})

return comment
}
}
}

Advanced Subscription Patterns

Filtered Subscriptions

Filter subscriptions based on user permissions:

const resolvers = {
Subscription: {
postCreated: {
subscribe: withFilter(
() => pubsub.asyncIterator(['POST_CREATED']),
(payload, _, context) => {
// Only subscribe to posts from followed users
return context.user.following.includes(payload.postCreated.authorId)
}
)
}
}
}

Subscription with Arguments

Handle subscription arguments:

const resolvers = {
Subscription: {
postUpdated: {
subscribe: withFilter(
() => pubsub.asyncIterator(['POST_UPDATED']),
(payload, { postId }, context) => {
// Only subscribe to specific post updates
return payload.postUpdated.id === postId
}
)
}
}
}

Real-time Features

Live Comments

Implement live comments with subscriptions:

// Schema
const typeDefs = `
type Subscription {
commentAdded(postId: ID!): Comment!
}

type Comment {
id: ID!
content: String!
author: User!
post: Post!
createdAt: DateTime!
}
`

// Resolver
const resolvers = {
Subscription: {
commentAdded: {
subscribe: withFilter(
(_, { postId }) => pubsub.asyncIterator([`COMMENT_ADDED_${postId}`]),
(payload, { postId }) => payload.commentAdded.postId === postId
)
}
}
}

// Client usage
const COMMENTS_SUBSCRIPTION = gql`
subscription OnCommentAdded($postId: ID!) {
commentAdded(postId: $postId) {
id
content
author {
id
name
}
createdAt
}
}
`

function PostComments({ postId }) {
const { data } = useSubscription(COMMENTS_SUBSCRIPTION, {
variables: { postId }
})

return (
<div>
{data?.commentAdded && (
<Comment comment={data.commentAdded} />
)}
</div>
)
}

Live Notifications

Implement real-time notifications:

// Schema
const typeDefs = `
type Subscription {
notificationReceived: Notification!
}

type Notification {
id: ID!
type: NotificationType!
message: String!
read: Boolean!
createdAt: DateTime!
}

enum NotificationType {
LIKE
COMMENT
FOLLOW
MENTION
}
`

// Resolver
const resolvers = {
Subscription: {
notificationReceived: {
subscribe: (_, __, context) => {
if (!context.user) {
throw new AuthenticationError('Not authenticated')
}
return pubsub.asyncIterator([`NOTIFICATION_${context.user.id}`])
}
}
}
}

// Client usage
const NOTIFICATION_SUBSCRIPTION = gql`
subscription OnNotificationReceived {
notificationReceived {
id
type
message
read
createdAt
}
}
`

function NotificationCenter() {
const { data } = useSubscription(NOTIFICATION_SUBSCRIPTION)

return (
<div>
{data?.notificationReceived && (
<Notification notification={data.notificationReceived} />
)}
</div>
)
}

Deployment Considerations

Serverless vs. Persistent Connections

When deploying subscriptions, consider:

  1. Serverless Environments

    • Use WebSocket-compatible serverless platforms
    • Consider using a message broker (Redis, RabbitMQ)
    • Implement connection pooling
  2. Traditional Servers

    • Handle WebSocket connections efficiently
    • Implement proper error handling
    • Monitor connection health

Scaling Subscriptions

For scaling subscriptions:

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

const options = {
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
password: process.env.REDIS_PASSWORD
}

const pubsub = new RedisPubSub({
publisher: new Redis(options),
subscriber: new Redis(options)
})

const resolvers = {
Subscription: {
postCreated: {
subscribe: () => pubsub.asyncIterator(['POST_CREATED'])
}
}
}

Connection Management

Handle WebSocket connections properly:

const wss = new WebSocketServer({
server,
path: '/graphql',
// Handle connection lifecycle
onConnect: (ctx) => {
console.log('Client connected')
},
onDisconnect: (ctx) => {
console.log('Client disconnected')
}
})

// Implement heartbeat
setInterval(() => {
wss.clients.forEach((client) => {
if (client.isAlive === false) {
return client.terminate()
}
client.isAlive = false
client.ping()
})
}, 30000)

Best Practices

  1. Error Handling

    • Handle connection errors
    • Implement reconnection logic
    • Log subscription errors
  2. Performance

    • Use connection pooling
    • Implement proper cleanup
    • Monitor subscription usage
  3. Security

    • Authenticate subscriptions
    • Validate subscription arguments
    • Rate limit subscriptions
  4. Monitoring

    • Track active subscriptions
    • Monitor WebSocket connections
    • Log subscription events

Example: Complete Subscription Setup

Here's a complete example of a subscription setup:

// server.ts
import { createYoga } from '@graphql-yoga/node'
import { createServer } from 'node:http'
import { WebSocketServer } from 'ws'
import { RedisPubSub } from 'graphql-redis-subscriptions'
import Redis from 'ioredis'

const redisOptions = {
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT
}

const pubsub = new RedisPubSub({
publisher: new Redis(redisOptions),
subscriber: new Redis(redisOptions)
})

const yoga = createYoga({
schema,
plugins: [
useGraphQLSSE()
]
})

const server = createServer(yoga)
const wss = new WebSocketServer({
server,
path: '/graphql'
})

// schema.ts
const typeDefs = `
type Subscription {
postCreated: Post!
postUpdated: Post!
postDeleted: ID!
commentAdded(postId: ID!): Comment!
}

type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
}

type Comment {
id: ID!
content: String!
author: User!
post: Post!
}
`

// resolvers.ts
const resolvers = {
Subscription: {
postCreated: {
subscribe: withFilter(
() => pubsub.asyncIterator(['POST_CREATED']),
(payload, _, context) => {
return context.user.following.includes(payload.postCreated.authorId)
}
)
},
postUpdated: {
subscribe: withFilter(
() => pubsub.asyncIterator(['POST_UPDATED']),
(payload, { postId }) => {
return payload.postUpdated.id === postId
}
)
},
commentAdded: {
subscribe: withFilter(
(_, { postId }) => pubsub.asyncIterator([`COMMENT_ADDED_${postId}`]),
(payload, { postId }) => {
return payload.commentAdded.postId === postId
}
)
}
},

Mutation: {
createPost: async (_, { input }, context) => {
const post = await context.prisma.post.create({
data: {
...input,
authorId: context.user.id
}
})

pubsub.publish('POST_CREATED', {
postCreated: post
})

return post
},

addComment: async (_, { postId, content }, context) => {
const comment = await context.prisma.comment.create({
data: {
content,
postId,
authorId: context.user.id
}
})

pubsub.publish(`COMMENT_ADDED_${postId}`, {
commentAdded: comment
})

return comment
}
}
}

Summary

In this chapter, we've covered:

  • WebSocket setup with GraphQL Yoga
  • Implementing subscription types and resolvers
  • Advanced subscription patterns
  • Real-time features implementation
  • Deployment considerations
  • Best practices for subscriptions

These patterns will help you build real-time features in your GraphQL applications. In the next chapter, we'll explore integrations with various frameworks and tools.