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.