# GraphQL Subscriptions with Graphql-Yoga
# Goals and Introduction
See the tutorial GraphQL subscriptions with Node.js and Express (opens new window) at https://blog.logrocket.com/graphql-subscriptions-nodejs-express (opens new window)
We’ll use predefined post data stored inside a JSON file postdata.js (opens new window) to perform the following operations:
getPosts
: read all postsgetPost
: read a specific post by IDupdatePost
: update a post and triggers an event to channelpost
deletePost
: delete a post and triggers an event to channelpost
createPost
: create a post and triggers an event to channelpost
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.
Post objects have the following structure:
type Post{
id:ID
title:String
subtitle:String
body:String
published:Boolean
author: String
upvotes: Int
downvotes: Int
commentCount: Int
}
2
3
4
5
6
7
8
9
10
11
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.
# App structure
Here is the structure of the app:
✗ tree -I node_modules
.
├── README.md # Write your report here
├── index.js # Starts the GraphQL server
├── package-lock.json
├── package.json
├── postData.js # Exports the json. Mimicks the database
├── resolvers.js # Has the logic to resolve for all queries, mutations, and subscriptions
└── typeDefs.gql # the GraphQL Schema
2
3
4
5
6
7
8
9
# GraphQL Yoga
We’ll use graphql-yoga (opens new window). You can follow this tutorial (opens new window) to learn more about graphql-yoga.
✗ jq '.dependencies' package.json
{
"graphql-yoga": "^1.18.3"
}
2
3
4
GraphQL Yoga uses server-sent-events[1] for the subscription protocol.
Server-Sent Events (SSE) is a server push (opens new window) technology enabling a client to receive automatic updates from a server via an HTTP connection, and describes how servers can initiate data transmission towards clients once an initial client connection has been established.
SSE are commonly used to send message updates or continuous data streams to a browser client and designed to enhance native, cross-browser streaming through a JavaScript API called EventSource[2], through which a client requests a particular URL in order to receive an event stream. The EventSource API is standardized as part of HTML5. The mime type for SSE is text/event-stream
.
# Resolvers Structure
Notice how resolvers (opens new window) is an object with three properties Query
, Mutation
, and Subscription
whose values are objects.
const resolvers = {
Query: {
getPosts() { ... },
getPost(parent, args) { ... }
},
Mutation: {
createPost(parent, args, { pubsub }) { ... },
updatePost(parent, args, { pubsub }) { ... },
deletePost(parent, args, { pubsub }) { ... },
},
Subscription: {
post: { ... }
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Each entry in the Query
object corresponds to the function resolver of the corresponding query.
# Queries
We’ll define two queries, getPost
and getPosts
. Here is the schema:
type Query {
getPosts: [Post!]!
getPost(query: String!): [Post]
}
2
3
4
and here are the resolvers:
const resolvers = {
Query: {
getPosts() {
return posts;
},
getPost(parent, args) {
return posts.filter((post) => {
const inBody = post.body.toLowerCase().includes(args.query.toLowerCase())
const inTitle = post.title.toLowerCase().includes(args.query.toLowerCase())
return inBody || inTitle;
});
}
},
Mutation: {
createPost(parent, args, { pubsub }) { ... },
updatePost(parent, args, { pubsub }) { ... },
deletePost(parent, args, { pubsub }) { ... },
},
Subscription: {
post: { ... }
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Testing the queries
This is an example of a query to get a post:
query {
getPost(query: "recusandae") {
id
title
body
author
published
}
}
2
3
4
5
6
7
8
9
The answer is:
{
"data": {
"getPost": [
{
"id": "7",
"title": "Et sunt in error et recusandae ut animi ut.",
"body": "magni adipisci voluptatibus",
"author": "Anabelle Sipes",
"published": true
}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# Mutations
# Schema
This is the schema for the mutation createPost
to create a Post
:
type Post{
id:ID
title:String
subtitle:String
body:String
published:Boolean
author: String
upvotes: Int
downvotes: Int
commentCount: Int
}
type Mutation{
createPost(
id:ID!
title:String
subtitle:String
body:String
published:Boolean
author: String!
upvotes: Int
downvotes: Int
commentCount: Int
): Post!
updatePost( ... ): Post!
deletePost(id: ID!): Post!
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Resolvers
Here is the correspondent resolver for createPost
:
Mutation: {
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!');
},
updatePost(parent, args, { pubsub }) { ... },
deletePost(parent, args, { pubsub }) { ... },
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Once the post is created, we’ll publish an event with the field mutation
set to CREATED
to all subscribers of the post
channel with the newly created post in the field data
.
pubsub.publish('post', {
post: {
mutation: 'CREATED',
data: { ...args }
}
});
2
3
4
5
6
Notice how we have got the pubsub
object from the context
.
createPost(parent, args, { pubsub }) { ... }
# The pubsub Object
The pubsub
object was created in the index.js
file:
const { GraphQLServer, PubSub } = require('graphql-yoga');
const pubsub = new PubSub()
const server = new GraphQLServer({
typeDefs,
resolvers,
context:{
pubsub
}
})
2
3
4
5
6
7
8
9
Observe how is passed inside the context argument of the GraphQLServer
constructor:
# Testing the mutations
Let us test the createPost
mutation. We'll send first an Invalid example:
mutation withError{
createPost(
id: 3, title: "hello", body: "world",
author: "Casiano") {
id
title
body
author
}
}
2
3
4
5
6
7
8
9
10
the answer is:
{
"data": null,
"errors": [
{
"message": "Post with same id already exist!",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"createPost"
]
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Notice how data
is null
and errors
is an array of errors.
Here is a second test, containing a valid mutation
mutation ok {
createPost(
id: 11, title: "hello", body: "world",
author: "Casiano") {
id
title
body
author
}
}
2
3
4
5
6
7
8
9
10
The answer is:
{
"data": {
"createPost": {
"id": "11",
"title": "hello",
"body": "world",
"author": "Casiano"
}
}
}
2
3
4
5
6
7
8
9
10
The code for the other mutations is similar to the one for createPost
.
# Subscriptions
Here is the schema for the subscription:
enum Crud {
CREATE
UPDATE
DELETE
}
type Subscription {
post: SubscriptionPayload
}
type SubscriptionPayload {
mutation: Crud
data: Post
}
2
3
4
5
6
7
8
9
10
11
12
13
14
The objects returned by the subscription will be of type SubscriptionPayload
. The SubscriptionPayload
object has two fields: mutation
and data
.
- The
mutation
field will contain the type of mutation that triggered the subscription, in the case of acreatePost
isCREATED
. - The
data
field will contain the data of the newly created post.
The resolver for post
uses thepubsub.asyncIterator
method 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:
Subscription: {
post: {
subscribe(parent, args, { pubsub }) {
return pubsub.asyncIterator('post');
}
}
}
2
3
4
5
6
7
Subscription resolvers are wrapped inside an object and need to be provided as the value for a subscribe
field. You also need to provide another field called resolve
that actually returns the data from the data emitted by the AsyncIterator
.
# Testing the subscription
Here follow two captures of the subscription in action. The first one is the mutation,
the second one shows the notification received by the subscription:
# Exercise
Update the example to use the latest version of GraphQL-yoga (v3). See Migration from Yoga V1 (opens new window)
# References
- GraphQL subscriptions with Node.js and Express (opens new window)
- GraphQL Yoga
- Other tutorials on GraphQL subscriptions
- Tutorial (opens new window) Building live chat app with GraphQL subscriptions
- Realtime GraphQL Subscriptions (opens new window) from GRAPHQL-NODE TUTORIAL. Written by Maira Bello: Build your own GraphQL server
# FootNotes
The EventSource interface (opens new window) is web content's interface to server-sent events. An EventSource instance opens a persistent connection to an HTTP server, which sends events in text/event-stream format. The connection remains open until closed by calling EventSource.10 oct ↩︎