Editor’s note: This post was last updated on 29 November 2021 to reflect updates to GraphQL.
Have you ever wondered how Facebook notifies you when a friend posts something? Or how Google Maps updates your location in real time? The answer to these and many other mysteries is GraphQL subscriptions. GraphQL subscriptions enable you to subscribe to events under a source stream and receive notifications in real time via the response stream when the selected event executes.
In this article, we’ll provide a basic understanding of GraphQL subscriptions for events on JSON data in a Node.js server. You can grab the full source code used in this tutorial on GitHub.
Before we get started, you should have the following:
Let’s get started!
Note: You can access the full source code for this project at this GitHub repository.
Query
objectmutation
objectsubscription
objectOnce a GraphQL subscription is executed, a persistent function is created on the server that maps an underlying source stream to a returned response stream, as seen in the diagram below:
(Source: GitHub)
GraphQL subscriptions differ from queries in the way that data is delivered to the client. Queries immediately return a single response, while the GraphQL subscriptions return a result every time data is published on a topic to which you have subscribed.
The GraphQL subscription method is facilitated by a publisher/subscriber mechanism that can handle event-driven systems efficiently and at scale. In a publisher/subscriber model, all messages and data flow according to the queue principle (first in, first out) and then to the subscriber:
Note: for production, it is recommended to use the pub/sub implementation of Redis.
There are many packages available on npm that you can use to implement the pub/sub model for GraphQL subscriptions. Below are some of the most commonly used packages:
graphql-yoga
 : a fully-featured GraphQL server with a focus on easy setup, performance, and a great developer experiencegraphql-subscriptions
: lets you wire GraphQL with a pub/sub system, like Redis, to implement GraphQL subscriptionsapollo-server-express
 : the Express and connect integration of the GraphQL server. Apollo server is a community-maintained, open source GraphQL server that works with many Node.js HTTP server frameworksWe’ll use the graphql-yoga
module because it is built on top of the other two packages, and it provides all necessary dependencies and server bindings with Node.js under the hood.
We’ll use predefined post data stored inside a JSON file to perform the following operations:
getPosts
 : read all postsgetPost
: read a specific post by IDupdatePost
: update a postdeletePost
: delete a postcreatePost
: create a postThen, we’ll add the subscription to the last three operations. Now, it’s time to get our hands dirty with some code! First, make a folder, name it whatever you like, and initialize it with a package.json
file as follows:
mkdir graphql-sub cd graphql-sub npm init -y
Next, install the required dependencies:
npm i graphql-yoga
Now, we’ll create all of our files:
mkdir src cd src touch index.js postData.json typeDefs.js resolver.js
index.js
is responsible for the GraphQL server creation with pub/sub, which we’ll see in a minute. postData.json
is the JSON file in which we will perform CRUD operations. You can either add the sample code from GitHub, or create an array of objects for a post with the following schema:
id:ID!
title:String!
subtitle:String!
body:String!
published:Boolean!
author: String!
upvotes: Int!
downvotes: Int!
commentCount: Int!
We use typeDefs.js
to create schemas for the operations above. Lastly, resolvers.js
has the logic to resolve for all queries, mutations, and subscriptions defined under typeDefs.js
. Inside typeDefs.js
, add the following code:
//type definitions and schemas - (operation and data structure) const typeDefs = ` type Query { getPosts(query: String):[Post!]! getPost(query: String):Post! } type Post{ id:ID! title:String! subtitle:String! body:String! published:Boolean! author: String! upvotes: Int! downvotes: Int! commentCount: Int! } type Mutation{ updatePost( id:ID! title:String! subtitle:String! body:String! published:Boolean! author: String! upvotes: Int! downvotes: Int! commentCount: Int! ): Post! deletePost(id: ID!): Post! createPost( id:ID! title:String! subtitle:String! body:String! published:Boolean! author: String! upvotes: Int! downvotes: Int! commentCount: Int! ): Post! } type Subscription { post: PostSubscriptionPayload! } type PostSubscriptionPayload { mutation: String! data: Post! } `; module.exports = typeDefs;
In addition to the normal schema definitions for queries and mutations, we have a type called Subscription
that is added on the post
object via SubscriptionPayload
, a custom type.
Therefore, each time a change is made to a post
object, an event will be triggered that returns the name of the mutation performed , either update
, delete
, or create
a post.
Now, let’s code the skeleton of our resolvers.js
file for the typeDefs
above:
const posts = require('./postData'); //Resolvers - This are the set of the function defined to get the desired output for the given API const resolvers = { Query:{ }, Mutation:{ }, Subscription:{ }, } module.exports = resolvers;
We imported postData
, then we added our resolver
object, which contains our Query
, Mutation
, and Subscription
objects. We’ll flesh out each object in the sections below.
Query
objectInside of our Query
object, we’ll define two queries, getPost
and getPosts
:
Query: { getPosts() { return posts; }, getPost(parent, args) { return posts.filter((post) => { const body = post.body.toLowerCase().includes(args.query.toLowerCase()); const title = post.title .toLowerCase() .includes(args.query.toLowerCase()); return body || title; }); }, },
mutation
objectWe’ll define three mutations, createPost
, updatePost
, and deletePost
inside our mutation
object.
createPost
The createPost
method checks whether a post for the ID already exists. If it does, we’ll throw an error to the GraphQL server. Otherwise, we’ll create the post from the args
object and add it to our posts JSON data:
createPost(parent, args, { pubsub }) { const id = parseInt(args.id, 10); const postIndex = posts.findIndex((post)=> post.id === id); if (postIndex === -1) { posts.push({ ...args }); pubsub.publish('post', { post:{ mutation: 'CREATED', data: {...args} } }); return {...args}; }; throw new Error('Post with same id already exist!'); }
Once the post is created, we’ll publish an event called CREATED
to all subscribers of the post
channel through the socket with the newly created post data in its payload.
updatePost
In the updatePost
method, the post with the specified ID will be updated with the contents of args
. If the post ID does not exist, we’ll throw an error:
updatePost(parent, args, { pubsub }) { const id = parseInt(args.id, 10); const postIndex = posts.findIndex((post) => post.id === id); if (postIndex !== -1) { const post = posts[postIndex]; const updatedPost = { ...post, ...args, }; posts.splice(postIndex, 1, updatedPost); pubsub.publish("post", { post: { mutation: "UPDATED", data: updatedPost, }, }); return updatedPost; } throw new Error("Post does not exist!"); },
As you can see, a new event called UPDATED
is published when the operation is successful.
deletePost
The deletePost
method deletes a post from the array if the ID exists. Afterwards, a new event called DELETED
is published with the deleted post data:
deletePost(parent, args, { pubsub }) { const id = parseInt(args.id, 10); const isPostExists = posts.findIndex((post)=> post.id === id); if (isPostExists === -1) { throw new Error('Post does not exist!'); } // splice will return the index of the removed items from the array object const [post] = posts.splice(isPostExists, 1); pubsub.publish('post', { post:{ mutation: 'DELETED', data: post } }); return post; },
subscription
objectLastly, we’ll add our subscription
object, which uses a pubsub.asyncIterator
function to map the event underlying the source stream to a returned response stream. The asyncIterator
takes the channel name through which the event across the app will be mapped out:
post:{ subscribe(parent, args, {pubsub}){ return pubsub.asyncIterator('post'); } }
Now, the only file left is index.js
. Add the following code to it:
const { GraphQLServer, PubSub } = require("graphql-yoga"); const typeDefs = require("./typeDefs"); const resolvers = require("./resolvers"); const pubsub = new PubSub(); const server = new GraphQLServer({ typeDefs, resolvers, context: { pubsub, }, }); const options = { port: 3000, }; server.start(options, ({ port }) => { console.log( `Graphql Server started, listening on port ${port} for incoming requests.` ); });
We created a GraphQL server, passed all our files, then started the server. Finally, we’ll add a script to run our project in package.json
:
"scripts": { "start": "node src/index.js" },
Open the terminal and run npm start
. If everything is working well, you’ll see the following message:
Graphql Server started, listening on port 3000 for incoming requests.
Now, head over to the browser and type http://localhost:3000
, and you’ll see a GraphQL Playground instance. To confirm that everything is working correctly, run a getPosts
query, as shown below:
query { getPosts { id, body, downvotes, title, subtitle, upvotes } }
To start our subscription to the post changes, we’ll open up a new tab in GraphQL Playground and run the following code:
subscription{ post{ mutation data{ id, title, subtitle, body, published author, upvotes, downvotes, commentCount, } } }
The code above enables us to add a subscription to our channel post and start listening for any event published in the channel:
To see our GraphQL subscription in action, just perform any of the mutations in a new tab. For example:
mutation { updatePost( id: 8, downvotes:3, author: "deepak gupta", published: true, subtitle: "testinng subtitle", body: "testing body", commentCount: 12, upvotes: 4, title: "oh yeah :)" ) { id } }
Navigate to the tab where the subscription query is running. Notice how the post response stream returned the data for the update
event:
To wrap up our tutorial, let’s quickly recap the subscription process. The subscription is defined below in typeDefs.js
:
type Subscription { post: PostSubscriptionPayload! } type PostSubscriptionPayload { mutation: String! data: Post! }
Use the pub/sub method provided by graphql-yoga
to subscribe and publish. The pub/sub method can also facilitate mechanisms like EventEmitter
:
const { GraphQLServer, PubSub } = require('graphql-yoga'); const typeDefs = require('./typeDefs'); const resolvers = require('./resolvers'); const pubsub = new PubSub(); const server = new GraphQLServer({ typeDefs, resolvers, context: { pubsub, }, });
Implement the subscription type resolver to map the event using pubsub.asyncIterator
. Once we request a subscription from the GraphQL Playground, pubsub.asyncIterator
will add our socket to its listening socket list and send back events while we call pubsub.publish
:
Subscription: { post: { subscribe(parent, args, { pubsub }) { return pubsub.asyncIterator('post'); }, }, }
Finally, call the pubsub.publish()
method from the mutation channel:
pubsub.publish('post', { post: { mutation: 'UPDATED', data: updatedPost, }, });
Now, you’ve created a GraphQL subscription, a real-time method to sync up client and server. By implementing GraphQL subscriptions as we’ve done in this tutorial, you can implement real time notifications whenever a selected event is executed. I hope you enjoyed this tutorial.
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries' key-value pairs.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
4 Replies to "GraphQL subscriptions with Node.js and Express"
Hi Deepak, seems that you copy pasted the code for the createPost also in the updatePost code snippet
Thanks so much for pointing this out, Nick. The post is updated now.
Hi do you have example of running this with Websocket ?
Great Tutorial. Thank you