GraphQL concepts I wish someone explained to me a year ago
Part 6: Subscriptions (server implementation)
In part 5 of this series we implemented mutations on the Bookstore client. In this part, we jump back to the server side and implement subscriptions.
One part will be released every day this week. Follow me or subscribe to my newsletter to get updates on this series.
Source Code
You can download the code for this part by executing the following commands. Skip the first command if you have already cloned the repository.
git clone https://github.com/nareshbhatia/graphql-bookstore.git
cd graphql-bookstore
git checkout 6-subscriptions-server
Test Drive
Before diving into the implementation, let’s look at how the final implementation works. Make sure you have yarn installed on your machine. Now execute the following commands:
cd apollo-bookstore-server
yarn
yarn dev
This will start the server. Point your browser to http://localhost:4000/ — it will show the GraphQL Playground. You’ve already used the Playground in part 2, so it needs no introduction.
Remember, subscriptions are a way to display changes in real time. Our discussion from part 1 states:
GraphQL subscriptions are a way to push data from the server to the clients in real-time. As defined by the GraphQL specification: “subscription is a long‐lived request that fetches data in response to source events”. In fact, subscriptions are very similar to queries in that they specify a set of fields to be returned. However, instead of returning them immediately, the server sends them to the requesting client every time a specified event happens.
Let’s start by looking at a subscription operation declared in the updated schema. Here’s the authorMutated
subscription:
type Subscription {
authorMutated: AuthorMutationPayload!
}
type AuthorMutationPayload {
mutation: MutationType!
node: Author!
}enum MutationType {
CREATED
UPDATED
DELETED
}
Here’s how to interpret this declaration:
If a client subscribes to the authorMutated
event, it will be sent the AuthorMutationPayload
whenever that event happens. AuthorMutationPayload
is an object with two fields:
- mutation: indicates what type of mutation was performed:
CREATED
,UPDATED
orDELETED
- node: The author that was created, updated or deleted
With this background, let’s subscribe to the authorMutated
event from the Playground. In the left panel, enter the following subscription and click the Execute button.
subscription {
authorMutated {
mutation
node {
id
name
}
}
}
The right panel will now show “Listening …”.
Open a new tab in Playground and create an author by entering this mutation
mutation CreateAuthor($name: String!) {
createAuthor(name: $name) {
id,
name
}
}
along with the following query variable:
{
"name": "Walter Isaacson"
}
When the author is successfully created, switch back to the subscription tab. You will see the following subscription response. Note that the mutation type is CREATED
.
{
"data": {
"authorMutated": {
"mutation": "CREATED",
"node": {
"id": "4otdl",
"name": "Walter Isaacson"
}
}
}
}
Now open another tab in Playground and update the name of the author by entering the following mutation:
mutation UpdateAuthor($authorId: ID!, $name: String!) {
updateAuthor(authorId: $authorId, name: $name) {
id,
name
}
}
along with the following query variables (use the author id that you received):
{
"authorId": "4otdl",
"name": "W Isaacson"
}
When the author is successfully updated, switch back to the subscription tab. You will see the following subscription response. Note that the mutation type is UPDATED
.
{
"data": {
"authorMutated": {
"mutation": "UPDATED",
"node": {
"id": "4otdl",
"name": "W Isaacson"
}
}
}
}
Subscription Implementation
Declaring the schema
The first step to implement a subscription is to declare it in our schema. We’ve already seen the declaration for the authorMutated
subscription. Look over the declarations of publisherMutated
and bookMutated
— they are similar.
Creating up a PubSub instance
The next step is to create a PubSub
instance which is responsible for managing subscriptions on the server. PubSub
is a class that exposes a publish
and a subscribe
API. Here’s the code that creates a global PubSub
instance to be used in our server:
# ----- graphql/pubsub.ts -----import { PubSub } from 'apollo-server';
export const pubsub = new PubSub();
Creating subscription resolvers
Just like other schema fields, we need to create resolvers for our subscription operations. However, instead of a function, the subscription resolvers requires an object with a subscribe
method that return an AsyncIterator
. We obtain the AsyncIterator
using our global pubsub
object. This iterator is responsible for emitting published messages to all the subscribed clients.
Putting this together, here’s the authorMutated
resolver.
# ----- graphql/resolvers/author-resolvers.ts -----Subscription: {
authorMutated: {
subscribe: () => pubsub.asyncIterator(AUTHOR_MUTATED)
}
}
The other subscription resolvers are similar. Take a look to see if they make sense.
Publishing subscription events
Now that we’ve provided a mechanism for clients to subscribe to events, the only thing left is to publish those events. For example, we need to publish the AUTHOR_MUTATED
event whenever an author is created or updated. Let’s enhance the mutation resolvers to do exactly that.
# ----- graphql/resolvers/author-resolvers.ts -----Mutation: {
createAuthor(parent, args) {
return dataSources.bookService
.createAuthor({
name: args.name
})
.then(author => {
pubsub.publish(AUTHOR_MUTATED, {
authorMutated: {
mutation: 'CREATED',
node: author
}
});
return author;
});
},
updateAuthor(parent, args) {
return dataSources.bookService
.updateAuthor(args.authorId, {
name: args.name
})
.then(author => {
pubsub.publish(AUTHOR_MUTATED, {
authorMutated: {
mutation: 'UPDATED',
node: author
}
});
return author;
});
}
}
Here we call pubSub.publish()
to publish theAUTHOR_MUTATED
event along with the desired payload.
Scaling subscriptions
By default, the Apollo Server uses a PubSub
implementation provided by graphql-subscriptions. This implementation does not scale well as described in the docs:
Note that the default PubSub implementation is intended for demo purposes. It only works if you have a single instance of your server and doesn’t scale beyond a couple of connections. For production usage you’ll want to use one of the PubSub implementations backed by an external store. (e.g. Redis)
To scale subscriptions beyond a single GraphQL server, we need a more robust pub/sub mechanism that can distribute published messages over multiple GraphQL servers. Here’s the logical architecture for such a system.
Note that in this architecture there are event sources and listeners other than the GraphQL servers themselves, e.g. the two micro-services shown on the left. In fact, if we upgrade to this architecture for our bookstore, I would recommend having a Bookstore micro-service. This micro-service would connect to our persistence data store and be responsible for all the CRUD operations. Moreover, publishing mutation events (which is currently happening inside the GraphQL resolvers) would move to the Bookstore micro-service, which is now responsible for mutations. In this scenario, the GraphQL servers will only subscribe to mutation events.
Let’s use the publish/subscribe capabilities of Redis to implement just the event bus for now (not the full architecture). We will switch to the graphql-redis-subscriptions library to do this. Follow the steps below:
- Install and start a Redis server. Here’s an article that describes how to install Redis on MacOS.
- Open
graphql/pubsub.ts
. Comment out the defaultPubSub
implementation (lines 2–3) and uncomment the Redis implementation (lines 7–8). - Restart the Bookstore server.
Now subscriptions will use the Redis server for publish/subscribe. This will scale significantly better.
Summary
We’ve now covered the subscription implementation on the server. This is what we learned:
- How to declare subscriptions in our schema
- How to create a
PubSub
instance to support subscriptions - How to write a subscription resolver
- How to publish subscription events
- How to scale subscriptions using a Redis implementation of
PubSub
With only one part left in the series, I’d love to get your questions and comments.
In part 7, we will implement subscriptions in our React client.
Read Part 7: Subscriptions (client implementation) >
Resources
- GraphQL Subscriptions by Robert Zhu: In this presentation, Robert starts from the official (abstract) definition of subscriptions and explains what it means in practical terms.
- GraphQL Subscriptions by Urigo: Uri demystifies the magic behind GraphQL subscriptions and shows how anyone can implement them.