Lesson 3
Setting Up Subscriptions for Real-Time Data
Lesson Overview

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.
Setting Up Apollo Server with Subscriptions

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 PubSub from graphql-subscriptions, which is an in-memory event system for publishing and subscribing to events. This allows our resolvers to notify clients about real-time updates.

TypeScript
1import { ApolloServer, gql } from 'apollo-server-express'; 2import express from 'express'; 3import { PubSub } from 'graphql-subscriptions'; 4import { v4 as uuidv4 } from 'uuid'; 5import { createServer } from 'http'; 6import { SubscriptionServer } from 'subscriptions-transport-ws'; 7import { execute, subscribe } from 'graphql'; 8import { makeExecutableSchema } from '@graphql-tools/schema'; 9 10// Initialize PubSub 11const pubsub = new PubSub();
Defining Schema and Resolvers with Subscriptions

In this section, we'll define the schema with a type definition that includes a Subscription type.

TypeScript
1const typeDefs = gql` 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.

Writing Resolver Functions

Next, we need to implement resolver functions for these subscriptions. When a new book is added via the addBook mutation, it publishes an event called BOOK_ADDED which PubSub handles, triggering the subscription. This allows subscribed clients to receive real-time updates about the new book.

TypeScript
1// 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.

Integrating WebSocket for Real-Time Updates

WebSockets provide a way for a server and a client to communicate in real-time over a single, long-lived connection. This is crucial for handling subscriptions.

We'll integrate WebSockets into our Apollo Server setup to handle subscriptions.

TypeScript
1// 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 context: ({ req }) => { 14 const token = req.headers.authorization || ''; 15 return { token }; 16 } 17}); 18 19// Start the Apollo server and Subscription server 20const startServer = async () => { 21 await server.start(); 22 server.applyMiddleware({ app }); 23 24 SubscriptionServer.create( 25 { schema, execute, subscribe }, 26 { server: httpServer, path: '/graphql' } 27 ); 28 29 httpServer.listen(4000, () => { 30 console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`); 31 console.log(`🚀 Subscriptions ready at ws://localhost:4000/graphql`); 32 }); 33}; 34 35startServer();

When you run this code, your server should be ready to handle real-time subscriptions.

Requesting Subscriptions after Setting Up the Server

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.

First, set up the required endpoints and data structures.

TypeScript
1import WebSocket from 'ws'; 2import { SubscriptionClient } from 'subscriptions-transport-ws'; 3import gql from 'graphql-tag'; 4import fetch from 'node-fetch'; 5import { ExecutionResult } from 'graphql'; 6 7// Type definitions for GraphQL responses 8interface Book { 9 id: string; 10 title: string; 11 author: string; 12} 13 14interface BooksQueryResponse { 15 books: Book[]; 16} 17 18interface AddBookMutationResponse { 19 addBook: Book; 20} 21 22interface BookAddedSubscriptionPayload { 23 bookAdded: Book; 24}
GraphQL Queries and Mutations

Define the queries and mutations that will be used in the client application.

TypeScript
1const getBooksQuery = gql` 2 query { 3 books { 4 id 5 title 6 author 7 } 8 } 9`; 10 11const addBookMutation = gql` 12 mutation($title: String!, $author: String!) { 13 addBook(title: $title, author: $author) { 14 id 15 title 16 author 17 } 18 } 19`; 20 21const bookAddedSubscription = gql` 22 subscription { 23 bookAdded { 24 id 25 title 26 author 27 } 28 } 29`;
Helper Function for Sending GraphQL Requests

Create a function to facilitate sending GraphQL requests.

TypeScript
1// Define the GraphQL endpoint 2const GRAPHQL_ENDPOINT = 'http://localhost:4000/graphql'; 3const WEBSOCKET_ENDPOINT = 'ws://localhost:4000/graphql'; 4 5const fetchGraphQL = async <T>(query: string, variables?: Record<string, any>): Promise<T> => { 6 const response = await fetch(GRAPHQL_ENDPOINT, { 7 method: 'POST', 8 headers: { 9 'Content-Type': 'application/json', 10 }, 11 body: JSON.stringify({ query, variables }), 12 }); 13 const result = (await response.json()) as ExecutionResult<T>; 14 if (!result.errors) { 15 return result.data as T; 16 } else { 17 throw new Error(`GraphQL error: ${result.errors.map((e: any) => e.message).join(', ')}`); 18 } 19};
Setting Up WebSocket for Subscriptions

Initialize the WebSocket client and set up the subscription for real-time updates.

TypeScript
1const client = new SubscriptionClient(WEBSOCKET_ENDPOINT, { reconnect: true }, WebSocket); 2 3const subscription = client.request({ query: bookAddedSubscription.loc?.source.body! }).subscribe({ 4 next(response) { 5 const data = response.data as { bookAdded: Book }; 6 if (data && data.bookAdded) { 7 console.log('Book added:', data.bookAdded); 8 } else { 9 console.error('Subscription response was null or undefined:', response); 10 } 11 12 delay(1000).then(() => { 13 subscription.unsubscribe(); 14 client.close(); 15 checkCompletion(); 16 }); 17 }, 18 error(error) { 19 console.error('Subscription error:', error); 20 client.close(); 21 checkCompletion(); 22 }, 23});
Executing Queries and Mutations

Execute the defined queries and mutations.

TypeScript
1let tasksCompleted = 0; 2const totalTasks = 3; 3 4function checkCompletion() { 5 tasksCompleted += 1; 6 if (tasksCompleted === totalTasks) { 7 console.log('All tasks completed.'); 8 process.exit(0); 9 } 10} 11 12// Fetch books 13fetchGraphQL<{ books: Book[] }>(getBooksQuery.loc?.source.body!) 14 .then((data) => console.log('Books:', data.books)) 15 .catch((error) => console.error('Error fetching books:', error)) 16 .finally(() => checkCompletion()); 17 18// Add a new book 19fetchGraphQL<{ addBook: Book }>(addBookMutation.loc?.source.body!, { title: '1984', author: 'George Orwell' }) 20 .then((data) => console.log('Added book:', data.addBook)) 21 .catch((error) => console.error('Error adding book:', error)) 22 .finally(() => checkCompletion());
Summary

In this lesson, we:

  • Discussed real-time data and its importance.
  • Introduced GraphQL Subscriptions and compared them with Queries and Mutations.
  • Set up Apollo Server with subscriptions.
  • Defined schema and resolver functions.
  • Integrated WebSockets for real-time updates.

You’re now ready to move on to the practice exercises. These will help you solidify your understanding by applying what you’ve learned hands-on.

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.