Skip to main content

GraphQL subscriptions with Node.js

GraphQL Subscriptions With Node.js
Ever wonder 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 modern mysteries is (among other things) GraphQL subscriptions.
High-Level Overview of GraphQL Subscriptions
(Source: GitHub)
In this article, we’ll provide a basic understanding of GraphQL subscriptions for events on JSON data in a Node.js server.
Before we get started, you should have:
  • Node.js installed
  • A basic understanding of GraphQL concepts such as schema, query, mutation, and resolve
Without further ado, let’s dive in.

What are GraphQL subscriptions?

GraphQL subscriptions enable you to subscribe to events under a source stream and receive notifications in real time via a response stream when a selected event executes. Once a GraphQL subscription is executed, a persistent function is created on the server that maps an underlying source stream to a returned response stream.
GraphQL subscriptions differ from queries in the way the data is delivered to the client. The latter immediately returns a single response, while the former returns a result every time data is published on a topic to which you have subscribed.
This is facilitated by a publisher/subscriber mechanism that can handle event-driven systems efficiently and at a 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.
Diagram Illustrating the Query Principle in the Context of GraphQL subscriptions
Note: for production, it is recommended to use the pub/sub implementation of Redis.
There are many packages available on npm that can be used to implement the pub/sub model for GraphQL subscriptions. Below are some of the most commonly used packages.
  • graphql-yoga  is a fully featured GraphQL server with focus on easy setup, performance, and a great developer experience
  • graphql-subscriptions lets you wire up GraphQL with a pub/sub system (such as Redis) to implement subscriptions in GraphQL
  • apollo-server-express  is the express and connect integration of GraphQL server. Apollo server is a community-maintained, open-source GraphQL server that works with many Node.js HTTP server frameworks
We will use the graphql-yoga module because it is built over the other two and provides all necessary dependency and server binding with Node.js under the hood. Don’t worry about those last two things; once you get a hang of the implementation, they will be a breeze.

What we will code

We’ll use the post data that is stored inside a JSON file, and we’ll perform the following operations.
  • getPosts  (read all posts)
  • getPost (read a specific post by ID)
  • updatePost  (update a post)
  • deletePost  (delete a post)
  • createPost  (create a post)
Then, 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 using Node.js.
mkdir graphql-sub
cd graphql-sub
npm init
Next, install the dependency required.
npm i --s graphql-yoga
Now we’ll create all our files.
touch index.js postData.json typeDefs.js resolver.js
  • index.js is responsible for the GraphQLServer creation with pub/sub, which we will see in a minute
  • postData.json is the JSON file on which we will perform CRUD. Add the following code or an array of an object for a post who’s schema should be:
    • id:ID!
    • title:String!
    • subtitle:String!
    • body:String!
    • published:Boolean!
    • author: String!
    • upvotes: Int!
    • downvotes: Int!
    • commentCount: Int!
  • typeDefs.js will be used to create schemas for the above operations
  • resolvers.js will have the logic to resolve for all queries, mutation, 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: SubscriptionPayload!
    }
    
    type SubscriptionPayload {
        mutation: String!
        data: Post!
    }
`;
module.exports = typeDefs;
Other than the normal schema definitions for queries and mutation, we have a type called Subscription that is added on the post object via a custom type  SubscriptionPayload.
Therefore, each time a change is made to a post object, an event will be triggered for all who subscribe to events that return the name of the mutation performed  —  update, delete, and create and post data.
Now let’s code our resolvers.js for the above typeDefs.
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;

Coding objects

We first imported the postData and then added our resolverobject, which contains our querymutation, and subscriptionobject.
Let’s code each object one by one .

Query object

We will define two queries — getPost and getPosts — inside our query object.
// return all posts
getPosts() {
  return posts;
},
// return post by args passed, for now it just check for body and 
// title for the post
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 object

We will define three mutations — createPost ,updatePost, and deletePost — inside our mutation object.

createPost

Check whether the post for the ID already exists. If yes, we’ll throw an error to GraphQL server. Otherwise, we’ll create the post from args 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!');
}
We published an event called CREATED that will be triggered to all subscribers of the channel post through the socket and return newly created post data.

updatePost

We will check whether the post for the ID already exists. If it does, we’ll update the post with the args passed. Otherwise, it’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, we again published a new event called UPDATEDthat returns the updated post data.

deletePost

We will check whether the post for the ID already exists. If it does, we’ll delete it from the posts array or throw an error.
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);
  // return post;
pubsub.publish('post', {
    post:{
        mutation: 'DELETED',
        data: post
    }
  })
  return post;
},
Again, we published a new event called DELETED with the delete post data.

Subscription object

This object 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 the index.js. Add the following code to this file.
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.`,
  )
})
Here, we created a GraphQLServer, passed all our files, and started the server.
Finally, we’ll add a script to run our project in package.json.
"scripts": {
  "start": "node index.js"
},
Open the terminal and run npm start. If everything is good, you’ll see the following message.
Graphql Server started, listening on port 3000 for incoming requests.
Now head over to the browser and type localhost:3000. You’ll see a GraphQL Playground.
Just to check that everything is working as expected, let’s run a getPosts query.
Running a getPosts Query in GraphQL
To start our subscription to the post changes, we’ll open up a new tab in GraphQL Playground and run the following.
subscription{
  post{
    mutation
    data{
      id,
      title,
      subtitle,
      body,
      published
      author,
      upvotes,
      downvotes,
      commentCount,
    }
  }
}
This enables us to add a subscription to our channel post and start listening for any event published in the channel.
Add GraphQL Subscriptions to Channel Post
To see it in action, just perform any of the mutations. 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
  } 
}
As you can see, the post response stream gave back the data for the update event.
Response Stream Data in a GraphQL Subscription

Recapping the GraphQL subscription process

To wrap up our tutorial, let’s quickly recap the subscription process. The subscription is defined below in typeDefs.js.
type Subscription {
post: SubscriptionPayload!
}
type SubscriptionPayload {
mutation: String!
data: Post!
}
Use the pub/sub method provided by graphql-yoga to subscribe and publish. This can also facilitate mechanisms like EventEmitter.
const { GraphQLServer, PubSub } = require('graphql-yoga');const pubsub = new PubSub()
const server = new GraphQLServer({
typeDefs,
resolvers,
context:{
pubsub
}
})
Implement the resolver for subscription type to map the event using pubsub.asyncIterator. Once we request a subscription from GraphQL Playground, it will add our socket to its listening socket list and send back events while we call pubsub.publish.
post:{
subscribe(parent, args, {pubsub}){
return pubsub.asyncIterator('post');
}
}
Finally, call the pubsub.publish() method from the channel added mutation.
pubsub.publish('post', {
post:{
mutation: 'UPDATED',
data: updatedPost
}
});
If you’ve followed these steps to a T, you’ve successfully created a GraphQL subscription, a real-time method to sync up client and server.

Comments

Popular posts from this blog

How to use Ngx-Charts in Angular ?

Charts helps us to visualize large amount of data in an easy to understand and interactive way. This helps businesses to grow more by taking important decisions from the data. For example, e-commerce can have charts or reports for product sales, with various categories like product type, year, etc. In angular, we have various charting libraries to create charts.  Ngx-charts  is one of them. Check out the list of  best angular chart libraries .  In this article, we will see data visualization with ngx-charts and how to use ngx-charts in angular application ? We will see, How to install ngx-charts in angular ? Create a vertical bar chart Create a pie chart, advanced pie chart and pie chart grid Introduction ngx-charts  is an open-source and declarative charting framework for angular2+. It is maintained by  Swimlane . It is using Angular to render and animate the SVG elements with all of its binding and speed goodness and uses d3 for the excellent math functio...

Understand Angular’s forRoot and forChild

  forRoot   /   forChild   is a pattern for singleton services that most of us know from routing. Routing is actually the main use case for it and as it is not commonly used outside of it, I wouldn’t be surprised if most Angular developers haven’t given it a second thought. However, as the official Angular documentation puts it: “Understanding how  forRoot()  works to make sure a service is a singleton will inform your development at a deeper level.” So let’s go. Providers & Injectors Angular comes with a dependency injection (DI) mechanism. When a component depends on a service, you don’t manually create an instance of the service. You  inject  the service and the dependency injection system takes care of providing an instance. import { Component, OnInit } from '@angular/core'; import { TestService } from 'src/app/services/test.service'; @Component({ selector: 'app-test', templateUrl: './test.component.html', styleUrls: ['./test.compon...

How to solve Puppeteer TimeoutError: Navigation timeout of 30000 ms exceeded

During the automation of multiple tasks on my job and personal projects, i decided to move on  Puppeteer  instead of the old school PhantomJS. One of the most usual problems with pages that contain a lot of content, because of the ads, images etc. is the load time, an exception is thrown (specifically the TimeoutError) after a page takes more than 30000ms (30 seconds) to load totally. To solve this problem, you will have 2 options, either to increase this timeout in the configuration or remove it at all. Personally, i prefer to remove the limit as i know that the pages that i work with will end up loading someday. In this article, i'll explain you briefly 2 ways to bypass this limitation. A. Globally on the tab The option that i prefer, as i browse multiple pages in the same tab, is to remove the timeout limit on the tab that i use to browse. For example, to remove the limit you should add: await page . setDefaultNavigationTimeout ( 0 ) ;  COPY SNIPPET The setDefaultNav...