Subscriptions in GraphQL

This is the fourth tutorial in the series about GraphQL, and in this tutorial, we are going to learn more about subscriptions, how they work and how to deal with real-time data in GraphQL.

I have already written posts about types, queries, and mutations in GraphQL. If you didn’t read them, I recommend you to do so before diving into this tutorial because you won't be able to catch up with what I'll be doing. So, let’s get started!

Getting Started

We already know ho queries and mutations work, so if we run our playground at localhost:4000/playground, in Docs we should all the queries and mutations that we have.

We know that with queries we can get data, and with mutations we can create, modify, or delete that data. But what about subscriptions? That’s what we’re going to learn now.

Intro to Subscriptions in GraphQL

Users expect from most of the apps today to give them real-time data, and it was difficult for developers working with REST APIs to build such a feature. With REST, we can’t get real-time data updates, and one of the most common hacks for this is to send a request from time to time (1min or so) and see if there’s new data available.

This approach kind of worked, but it wasn’t one of the best practices and it was kind of annoying. We needed a better way to get real-time updates without investing any additional effort.

Let’s imagine we have a messaging app, which can show real-time updates as notifications, new messages, new invites, etc. With GraphQL, we can get the exact data we need, through a single endpoint, and we can also work with real-time updates pretty easily with subscriptions.

Subscriptions in GraphQL are the way we can get real-time updates from the server. Basically, it is a request that asks the server to push multiple results to the client in response to some server-side trigger.

To work with subscriptions, we need to specify the request we want to send, and subscribe to it. Every time data changes, we’re going to get notified. With a subscription request we specify that we want to receive an event notification every time there's some change in the data.

    subscription {
      books {
        _id
        title
        description
        language  
        author {
           _id
           firstName
           lastName
           age
        }
      }
    }

Subscriptions work in a similar manner to queries or mutations, but we’re subscribing to a specific event, and every time we add a new book or remove one, we’re going to get notified.

So now, let’s get started and write our first subscriptions.

Resolvers

To write our first subscription, we need to make some adjustments in our server configuration. Go to our utils folder, and inside that folder go to context.js and replace all the code with the following code:

    import { PubSub } from "graphql-yoga";
    import { models } from "./models";

    const pubsub = new PubSub();

    export const context = () => ({
        models,
        pubsub
    });

The PubSub from graphql-yoga is what we'll use to publish/subscribe to channels. We passed it as a context to our GraphQL server, so that way, we’ll be able to access it in our resolvers.

Now, let’s write the types for our subscriptions. Firstly, you need to define the defined of the data that you want to subscribe to.

Go to our Book folder, inside the typeDefs file, write a new type called Subscription. We’ll define three subscriptions for now:

    type Subscription {
        books: [Book!]!
        bookAdded: Book!
        bookDeleted: Book!
    }

We added a books subscription, which will get all the books that we have in real-time. We also added a bookAdded subscription, that's going to get all the books that were added, and we also defined a bookRemoved subscription that will show us in real-time every book that was deleted.

Now, let’s write the code for these subscriptions. Inside our Book folder, go to our resolvers file. We’re going to define three consts at the top of our file:

    const books = "books";
    const bookAdded = "bookAdded";
    const bookDeleted = "bookDeleted";

These consts will be the identifier for our subscriptions. Now, inside the resolvers object, add a new object called Subscription, that’s going to look like this:

    Subscription: {
        books: {
            subscribe: (parent, args, { pubsub }) => pubsub.asyncIterator(books)
        },
        bookAdded: {
            subscribe: (parent, args, { pubsub }) =>
            pubsub.asyncIterator(bookAdded)
        },
        bookDeleted: {
            subscribe: (parent, args, { pubsub }) =>
            pubsub.asyncIterator(bookDeleted)
        }
    }

Inside that Subscription object, we passed three functions, that are the three subscriptions that we’re going to have. We pass the pubsub in the context, and inside we pass an async iterator function and the const name for each one that we defined earlier.

Now, we need to pass our subscriptions inside our mutations, because when we update the data, we need to get it in real-time. Inside our createBook mutation, we’re going to pass the pubsub in the context, and inside the promise of our mutation, we’re going to pass the following code:

    pubsub.publish(books, {
        books: Book.find()
            .populate()
            .then(books => books)
            .catch(err => err)
    }),
    pubsub.publish(bookAdded, { bookAdded: newBook })

We passed two pubsub, but why? Firstly, when we create a book we need to update so now, our books subscriptions will be returned with the new Book, and we also returned a specific subscription called bookAdded, when we add a new Book this subscription will return this specific Book.

Now, inside our deleteBook mutation, we’re going to put the following code:

    pubsub.publish(books, {
        books: Book.find()
            .populate()
            .then(books => books)
            .catch(err => err)
    });
    pubsub.publish(bookDeleted, { bookDeleted: Book.findById(_id) });

When we delete a book we need to update so now, our books subscriptions will be returned without the Book that got deleted, and we also returned a specific subscription called bookDeleted, when we delete a Book this subscription will return this specific Book.

Now, our whole resolvers.js file should look like this:

    import Author from "../../models/Author";
    import Book from "../../models/Book";

    const books = "books";
    const bookAdded = "bookAdded";
    const bookDeleted = "bookDeleted";

    const resolvers = {
        Query: {
            getBook: async (parent, { _id }, context, info) => {
                return await Book.findById(_id)
                    .populate()
                    .then(book => book)
                    .catch(err => err);
            },
            getAllBooks: async (parent, args, context, info) => {
                return await Book.find()
                    .populate()
                    .then(books => books)
                    .catch(err => err);
            }
        },
        Mutation: {
            createBook: async (
                parent,
                { title, description, language, author },
                { pubsub },
                info
            ) => {
                const newBook = await new Book({
                    title,
                    description,
                    language,
                    author
                });

                return new Promise((resolve, reject) => {
                    newBook.save((err, res) => {
                        err
                            ? reject(err)
                            : resolve(
                                  res,
                                  pubsub.publish(books, {
                                      books: Book.find()
                                          .populate()
                                          .then(books => books)
                                          .catch(err => err)
                                  }),
                                  pubsub.publish(bookAdded, { bookAdded: newBook })
                              );
                    });
                });
            },
            deleteBook: async (parent, { _id }, { pubsub }, info) => {
                pubsub.publish(books, {
                    books: Book.find()
                        .populate()
                        .then(books => books)
                        .catch(err => err)
                });
                pubsub.publish(bookDeleted, { bookDeleted: Book.findById(_id) });
                return await Book.findOneAndDelete({ _id });
            }
        },
        Subscription: {
            books: {
                subscribe: (parent, args, { pubsub }) => pubsub.asyncIterator(books)
            },
            bookAdded: {
                subscribe: (parent, args, { pubsub }) =>
                    pubsub.asyncIterator(bookAdded)
            },
            bookDeleted: {
                subscribe: (parent, args, { pubsub }) =>
                    pubsub.asyncIterator(bookDeleted)
            }
        },
        Book: {
            author: async ({ author }, args, context, info) => {
                return await Author.findById(author);
            }
        }
    };

    export default resolvers;

We’re going to write our Author subscriptions, so go to our Author folder and inside the typeDefs file, write a new type called Subscription. We’re going to define three subscriptions for now:

    Subscription: {
        authors: {
            subscribe: (parent, args, { pubsub }) => pubsub.asyncIterator(authors)
        },
        authorAdded: {
            subscribe: (parent, args, { pubsub }) =>
                pubsub.asyncIterator(authorAdded)
        },
        authorDeleted: {
            subscribe: (parent, args, { pubsub }) =>
                pubsub.asyncIterator(authorDeleted)
        }
    },

We need to pass our subscriptions inside our mutations as we did earlier, so inside our createAuthor mutation, we’re going to pass the pubsub in the context, and inside the Promise of our mutation, we’re going to pass the following code:

    pubsub.publish(authors, {
        authors: Author.find()
            .populate()
            .then(authors => authors)
            .catch(err => err)
    }),
    pubsub.publish(authorAdded, {
        authorAdded: newAuthor
    })

Now, inside our deleteAuthor mutation, we’re going to put the following code:

    pubsub.publish(authors, {
        authors: Author.find()
            .populate()
            .then(authors => authors)
            .catch(err => err)
    });
    pubsub.publish(authorDeleted, {
        authorDeleted: Author.findById(_id)
    });

When we delete an author we need to update and our authors subscriptions will be returned without the Author that got deleted, and we also returned a specific subscription called authorDeleted, when we delete a Author this subscription will return this specific Author.

Our whole resolvers.js file should look like this:

    import Author from "../../models/Author";
    import Book from "../../models/Book";

    const authors = "authors";
    const authorAdded = "authorAdded";
    const authorDeleted = "authorDeleted";

    const resolvers = {
        Query: {
            getAuthor: async (parent, { _id }, context, info) => {
                return await Author.findById(_id)
                    .populate()
                    .then(author => author)
                    .catch(err => err);
            },
            getAllAuthors: async (parent, args, context, info) => {
                return await Author.find()
                    .populate()
                    .then(authors => authors)
                    .catch(err => err);
            }
        },
        Mutation: {
            createAuthor: async (
                parent,
                { firstName, lastName, age },
                { pubsub },
                info
            ) => {
                const newAuthor = await new Author({
                    firstName,
                    lastName,
                    age
                });

                return new Promise((resolve, reject) => {
                    newAuthor.save((err, res) => {
                        err
                            ? reject(err)
                            : resolve(
                                  res,
                                  pubsub.publish(authors, {
                                      authors: Author.find()
                                          .populate()
                                          .then(authors => authors)
                                          .catch(err => err)
                                  }),
                                  pubsub.publish(authorAdded, {
                                      authorAdded: newAuthor
                                  })
                              );
                    });
                });
            },
            deleteAuthor: async (parent, { _id }, { pubsub }, info) => {
                pubsub.publish(authors, {
                    authors: Author.find()
                        .populate()
                        .then(authors => authors)
                        .catch(err => err)
                });
                pubsub.publish(authorDeleted, {
                    authorDeleted: Author.findById(_id)
                });
                return await Author.findOneAndDelete({ _id });
            }
        },
        Subscription: {
            authors: {
                subscribe: (parent, args, { pubsub }) => pubsub.asyncIterator(authors)
            },
            authorAdded: {
                subscribe: (parent, args, { pubsub }) =>
                    pubsub.asyncIterator(authorAdded)
            },
            authorDeleted: {
                subscribe: (parent, args, { pubsub }) =>
                    pubsub.asyncIterator(authorDeleted)
            }
        },
        Author: {
            books: async ({ _id }, args, context, info) => {
                return await Book.find({ author: _id });
            }
        }
    };

    export default resolvers;

Running our subscriptions

Now that we wrote all of our subscriptions, let’s go back to our playground at localhost and test them. First, we’re going to test the bookAdded subscription.

So, first you need to run the subscription:

    subscription {
      bookAdded {
         _id
        title
        description
        language  
        author {
           _id
           firstName
           lastName
           age
        }
      }
    }

Now, create a new book with any name that you want. After you create the book, you’re going to see this new book in the subscription, like this:

Our subscriptions are working fine, now let’s try to delete this book and run the bookDeleted subscription.

    subscription {
      bookDeleted {
         _id
        title
        description
        language  
        author {
           _id
           firstName
           lastName
           age
        }
      }
    }

First, you run the subscription, then you delete the book, and you should get a similar result to this one:

Our subscriptions are working fine, and if you want to try to run the Book subscriptions, I’d really recommend you to do it! It’s pretty similar to the Author subscriptions.

Subscriptions in GrapgQL are one of the best ways that we have to work with real-time data in APIs today, so it’s a nice concept to learn more about.

Conclusion

In this tutorial, we learned about subscriptions in GraphQL and how they work, and in the next and final tutorial in the series we’re going to learn more about authentication in GraphQL.

Here's the repo containing all the code we've gone through today.

So, stay tuned and see you in the next tutorial!

Write your comment…

Be the first one to comment