Welcome to this lesson on "Setting Up Subscriptions for Real-Time Data". In this lesson, we will discuss real-time data and subscriptions to events in GraphQL.
GraphQL Subscriptions enable clients to listen for real-time updates from the server. When an event that matches the subscription’s criteria occurs, the server sends the updated data to the client automatically.
This is how subscriptions differ from Queries and Mutations:
- Queries: Request data from the server.
- Mutations: Modify data on the server.
- Subscriptions: Receive updates whenever data is changed as specified.
Let's begin by setting up our Apollo Server
to handle subscriptions. We'll start by initializing our server and defining our schema, including the Subscription
type.
We'll also be using graphql-ws
for WebSocket communication, which allows our resolvers to notify clients about real-time updates.
TypeScript1import { ApolloServer } from '@apollo/server'; 2import { expressMiddleware } from '@apollo/server/express4'; 3import { makeExecutableSchema } from '@graphql-tools/schema'; 4import express from 'express'; 5import { PubSub } from 'graphql-subscriptions'; 6import { useServer } from 'graphql-ws/lib/use/ws'; 7import { createServer } from 'http'; 8import { WebSocketServer } from 'ws'; 9import { v4 as uuidv4 } from 'uuid'; 10 11// Initialize PubSub 12const pubsub = new PubSub();
In this section, we'll define the schema with a type definition that includes a Subscription type.
TypeScript1const typeDefs = `#graphql 2 type Book { 3 id: ID! 4 title: String! 5 author: String! 6 } 7 8 type Query { 9 books: [Book] 10 } 11 12 type Mutation { 13 addBook(title: String!, author: String!): Book 14 } 15 16 type Subscription { 17 bookAdded: Book 18 } 19`;
The Subscription
type defines a bookAdded
field, which is of type Book
.
Next, we need to implement resolver functions for these subscriptions. We will use Pubsub
for it.
Pubsub
serves as an event bus that allows publishing events (pubsub.publish
) and subscribing to those events (pubsub.asyncIterator
). In this case, it manages the event stream for the BOOK_ADDED
event, enabling clients to subscribe to notifications about newly added books.
Whenever the server publishes a BOOK_ADDED
event, the asyncIterator
sends the event's data (bookAdded: newBook
) to all subscribed clients. Each event in this stream corresponds to a new book being added.
This allows subscribed clients to receive real-time updates about the new book.
TypeScript1// Sample data 2let books = [ 3 { id: '1', title: 'The Hobbit', author: 'J.R.R. Tolkien' }, 4 { id: '2', title: 'Harry Potter', author: 'J.K. Rowling' }, 5]; 6 7const resolvers = { 8 Query: { 9 books: () => books, 10 }, 11 Mutation: { 12 addBook: (_: any, { title, author }: { title: string, author: string }) => { 13 const newBook = { id: uuidv4(), title, author }; 14 books.push(newBook); 15 pubsub.publish('BOOK_ADDED', { bookAdded: newBook }); 16 return newBook; 17 }, 18 }, 19 Subscription: { 20 bookAdded: { 21 subscribe: () => pubsub.asyncIterator(['BOOK_ADDED']), 22 }, 23 }, 24};
Here, when a book is added using the addBook
mutation, the new book data is sent to all clients subscribing to the bookAdded
subscription through the pubsub.publish
method.
WebSockets are a communication protocol that enables two-way, persistent communication between a client and a server. Unlike traditional HTTP, where each request-response cycle creates a new connection, WebSockets keep a single connection open, enabling real-time data exchange without waiting for client requests. Whenever an event occurs, the server sends data to the connected client over the WebSocket.
We'll integrate WebSockets into our Apollo Server
setup using graphql-ws
to handle subscriptions.
TypeScript1// Define the schema 2const schema = makeExecutableSchema({ typeDefs, resolvers }); 3 4// Initialize the Express application 5const app = express(); 6 7// Create the HTTP server 8const httpServer = createServer(app); 9 10// Initialize Apollo Server 11const server = new ApolloServer({ 12 schema, 13 plugins: [ 14 { 15 async serverWillStart() { 16 return { 17 async drainServer() { 18 subscriptionServer.close(); 19 }, 20 }; 21 }, 22 }, 23 ], 24}); 25 26// Apply express middleware 27app.use('/graphql', expressMiddleware(server)); 28 29// Start the Apollo server 30server.start().then(() => { 31 httpServer.listen(4000, () => { 32 console.log(`🚀 Server ready at http://localhost:4000/graphql`); 33 }); 34 35 // Create WebSocket server 36 const wsServer = new WebSocketServer({ 37 server: httpServer, 38 path: '/graphql', 39 }); 40 41 // Use GraphQL WS for subscriptions 42 useServer({ schema }, wsServer); 43});
When you run this code, your server should be ready to handle real-time subscriptions.
After setting up the server to handle subscriptions, it's essential to know how to request and subscribe to real-time data updates. Below, we will provide step-by-step instructions for setting up a client to request subscriptions using graphql-ws
.
Define subscription queries in the client application:
TypeScript1import { createClient } from 'graphql-ws'; 2 3// Define the WebSocket endpoint 4const WEBSOCKET_ENDPOINT = 'ws://localhost:4000/graphql'; 5 6// Define the subscription query 7const bookAddedSubscription = gql` 8 subscription { 9 bookAdded { 10 id 11 title 12 author 13 } 14 } 15`; 16 17// Initialize the WebSocket client 18const client = createClient({ 19 url: WEBSOCKET_ENDPOINT, 20}); 21 22client.subscribe( 23 { 24 query: bookAddedSubscription.loc?.source.body!, 25 }, 26 { 27 next: (data) => { 28 console.log('Book added:', data.data.bookAdded); 29 }, 30 error: (err) => console.error('Subscription encountered an error:', err), 31 complete: () => console.log('Subscription completed'), 32 } 33);
Execute the defined queries and mutations.
TypeScript1import fetch from 'node-fetch'; 2 3// Define the GraphQL endpoint 4const GRAPHQL_ENDPOINT = 'http://localhost:4000/graphql'; 5 6// Type definitions for GraphQL responses 7interface Book { 8 id: string; 9 title: string; 10 author: string; 11} 12 13const getBooksQuery = gql` 14 query { 15 books { 16 id 17 title 18 author 19 } 20 } 21`; 22 23const addBookMutation = gql` 24 mutation($title: String!, $author: String!) { 25 addBook(title: $title, author: $author) { 26 id 27 title 28 author 29 } 30 } 31`; 32 33const fetchGraphQL = async <T>(query: string, variables?: Record<string, any>): Promise<T> => { 34 const response = await fetch(GRAPHQL_ENDPOINT, { 35 method: 'POST', 36 headers: { 37 'Content-Type': 'application/json', 38 }, 39 body: JSON.stringify({ query, variables }), 40 }); 41 const result = await response.json(); 42 if (!result.errors) { 43 return result.data as T; 44 } else { 45 throw new Error(`GraphQL error: ${result.errors.map((e: any) => e.message).join(', ')}`); 46 } 47}; 48 49// Fetch books 50fetchGraphQL<{ books: Book[] }>(getBooksQuery.loc?.source.body!) 51 .then((data) => console.log('Books:', data.books)) 52 .catch((error) => console.error('Error fetching books:', error)); 53 54// Add a new book 55fetchGraphQL<{ addBook: Book }>(addBookMutation.loc?.source.body!, { title: '1984', author: 'George Orwell' }) 56 .then((data) => console.log('Added book:', data.addBook)) 57 .catch((error) => console.error('Error adding book:', error));
In this lesson, we:
- Discussed real-time data and its importance.
- Introduced GraphQL Subscriptions and compared them with
Queries
andMutations
. - Set up
Apollo Server
with subscriptions usinggraphql-ws
. - Defined schema and resolver functions.
- Integrated WebSockets for real-time updates.
By following this lesson, you have learned how to handle real-time subscriptions using Apollo Server 4 and graphql-ws
. You’re now ready to move on to the practice exercises.