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!