Get notified whenever ice cream is dispensed in your favourite flavour using real-time GraphQL subscriptions
GraphQL is often praised for being a step forward from data retrieval by making calls to REST APIs from your application’s front-end. Sharing a common schema language puts front-end and middle/back-end developers on the same page. As a front-end developer, you can easily fetch just the data you need with a single call, instead of having to call a bunch of endpoints in series to satisfy your query. Much of the data stitching can now be offloaded to an intermediary graphQL server that will call the underlying data provider services, such as REST APIs, databases and microservices, so you don’t have to.
With the introduction of graphQL subscriptions in 2017, you can now use graphQL schema language to subscribe to to event streams and provide real-time updates to the client. Using the Apollo server package, you can quickly set up a production-ready graphQL server that supports subscriptions over a websocket. Combine this with a regular express server and witness the sensational power of graphQL subscriptions.
Today we’re building a graphQL api for an ice cream shop. Whenever an ice cream is dispensed, we want to be notified. Also, we want to be able to pass a parameter which specifies the ice flavour we want to be notified about.
Getting started
Apollo graphQL’s apollo-server
delivers you a production-ready graphQL server without too much hassle. Apollo server can wrap a web server that you already have, like Express.
npm install apollo-server-express express graphql
Include the required packages in your main JS file
const express = require('express');
const { createServer } = require('http');
const { ApolloServer, gql } = require('apollo-server-express');
GraphQL schema and resolvers
Let’s start by writing up our types. So far, we need the default required Query type and a basic IceCream type to perform basic graphQL queries. As we’d like to retrieve all ice cream flavours, as well as a single ice cream by flavour, we’ll add the iceCream and iceCreams queries.
const typeDefs = gql`
type IceCream {
id: Int!
flavour: String!
description: String!
}
type Query {
iceCream(flavour: String!): IceCream
iceCreams: [IceCream]
}
`
To gather the data that our client request, which is described by the schema, we’ll write resolvers. Resolvers are handler functions for graphQL, which run in response to an incoming request, fetch data from somewhere, and return it to the client in the desired format. Since we don’t have a real back-end to talk to, we’ll have a stub contain our ice cream.
// The excellent flavour descriptions courtesy of https://phrasegenerator.com/wine, with modifications.
const stub = [
{
id: 0,
flavour: 'vanilla',
description: 'A flippant pepper bouquet and alcoholic garlic essences are blended in'
},{
id: 1,
flavour: 'strawberry',
description: 'Blends indigestible parsnip flavors with a sandy cool ranch flavor'
},{
id: 2,
flavour: 'pear',
description: 'A soporiphic coconut finish and enticing Bar-B-Q midtones are intertwined'
}
]
const resolvers = {
Query: {
iceCreams: () => {
return Promise.resolve(stub)
},
iceCream: (_, { flavour }) => {
return Promise.resolve(stub.find(({ flavour:stubFlavour }) => flavour === stubFlavour))
}
}
};
Querying for iceCreams will return the entire dataset, which contains three items. If you query for iceCream, you’re expected to pass the flavour you need as a string. The flavour argument is then used to look up an ice cream of which the flavour property (destructured to stubFlavour here) is equal to the flavour argument value.
Run the server
Create an instance of ApolloServer
and pass it the typeDefs and resolvers that were created earlier as an options object.
const server = new ApolloServer({
typeDefs,
resolvers
})
Next, make a regular express app and call the Apollo server method applyMiddleware to hook the Apollo server in to it.
const app = express()
server.applyMiddleware({ app })
Pass the express app to node’s http.createServer and call listen on it to start the server. The server will listen on http://localhost:3000
const httpServer = createServer(app)
const port = 3000
httpServer.listen(port, () => {
console.log(`Server ready at http://localhost:${port}${server.graphqlPath}`)
})
We’re using an express app here because we want to add some endpoints of our own at a later stage.
GraphQL playground
As soon as you have your server running, you can access a graphQL playground at its default url http://localhost:3000/graphql
Lets a query for a vanilla ice cream
query {
iceCream(flavour: "vanilla") {
flavour
description
}
}
This returns ice cream data with the properties we asked for
{
"data": {
"iceCream": {
"description": "A flippant pepper bouquet and alcoholic garlic essences are blended in",
"flavour": "vanilla"
}
}
}
You can also retrieve a list of all available ice cream with the iceCreams
query
query {
iceCreams {
flavour
description
}
}
Real-time ice cream
So far, we’re able to get ice cream data if we ask for it explicitly, which is very nice. However, we wanted real time updates each time ice cream is dispensed, so what’s up with that?
To be able to handle subscriptions, we need a component that is able to subscribe to and publish events. The graphql-subscriptions
package provides this functionality
npm install graphql-subscriptions
const express = require('express');
const { createServer } = require('http');
const { ApolloServer, gql, withFilter } = require('apollo-server-express');
const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub();
Let’s change the typeDefs variable and add a Subscription definition for an iceCreamDispensed event
const typeDefs = gql`
type IceCream {
id: Int!
flavour: String!
description: String!
owner: String!
}
type Query {
iceCream(flavour: String!): IceCream
iceCreams: [IceCream]
}
type Subscription {
iceCreamDispensed(flavour: String): IceCream
}
`
We’ll also need to add a resolver to handle subscriptions to the iceCreamDispensed event.
// Event name to listen to
const ICE_CREAM_DISPENSED = 'ICE_CREAM_DISPENSED';
const resolvers = {
Subscription: {
iceCreamDispensed: {
subscribe: () => pubsub.asyncIterator([ ICE_CREAM_DISPENSED ]),
}
},
// ... remaining part of resolvers stays the same
Query: {
And finally, we’ll attach the capability to accept websocket connections to our http server. call installSubscriptionHandlers passing the plain httpServer
server.applyMiddleware({ app })
const httpServer = createServer(app)
server.installSubscriptionHandlers(httpServer)
const port = 3000
httpServer.listen(port, () => {
console.log(`Server ready at http://localhost:${port}${server.graphqlPath}`)
console.log(`Subscriptions ready at ws://localhost:${port}${server.subscriptionsPath}`)
})
With this defined and the server running, we can use the subscription type in our graphQL playground
subscription {
iceCreamDispensed {
flavour
description
}
}
Run this and you’ll be subscribed to the event, but because ICE_CREAM_DISPENSED
is not being fired anywhere, you won’t see any data coming in.
We’ll leverage the express app that our graphQL server is mounted on to expose an endpoint that can be POSTed to to trigger the dispensing of a random ice cream flavour.
Inside the /dispatch handler, a random ice cream flavour is selected and picked from the stub data. Then we call pubsub.publish with an event name of ICE_CREAM_DISPENSED
and our randomly selected ice cream. The subscription resolver that is listening for this event, will react by emitting the data to the subscribers.
const app = express()
app.post('/dispatch', (req, res) => {
const flavours = ['vanilla','pear','strawberry']
const flavour = flavours[Math.round(Math.random() * (flavours.length - 1))]
const ice = stub.find(item => item.flavour === flavour)
pubsub.publish(ICE_CREAM_DISPENSED, { iceCreamDispensed: ice } )
return res.status(202).send('accepted')
})
server.applyMiddleware({ app })
Re-start the server and send an http request to the /dispatch endpoint
curl -X POST http://localhost:3000/dispatch
When you are subscribed, you should see data coming in on the right in the playground.
{
"data": {
"iceCreamDispensed": {
"flavour": "strawberry",
"description": "Blends indigestible parsnip flavors with a sandy cool ranch flavor"
}
}
}
Event filtering
Nice, but not quite what we wanted. Now we’re subscribed to all flavours. But what if we were only interested in events pertaining to a particular flavour? Our iceCreamDispensed resolver is not yet doing anything with the flavour argument that we pass to it. But it should. How? We can use the withFilter
function provided by the apollo-server-express
package.
withFilter
accepts a function returning an AsyncIterator and a filter function, which should return a boolean or a Promise of a boolean. The filter function decides whether or not the event will be emitted to a subscriber, and has access to the arguments that were passed with the subscription.
First, import withFilter
const express = require('express');
const { createServer } = require('http');
const { ApolloServer, gql, withFilter } = require('apollo-server-express');
const { PubSub } = require('graphql-subscriptions');
Change the iceCreamDispensed
resolver to use withFilter
const resolvers = {
Subscription: {
iceCreamDispensed: {
subscribe: withFilter(
() => pubsub.asyncIterator([ ICE_CREAM_DISPENSED ]),
({ iceCreamDispensed: { flavour:payloadFlavour } }, { flavour:requestFlavour }) => {
if (!requestFlavour) {
return true
}
return payloadFlavour === requestFlavour
}
)
},
},
Query: {
// ...same as before
Pass a flavour argument to your subscription in the playground
subscription {
iceCreamDispensed (flavour: "vanilla") {
flavour
description
}
}
Now, you’ll only receive events about vanilla ice cream over your subscription.
const express = require('express');
const { createServer } = require('http');
const { ApolloServer, gql, withFilter } = require('apollo-server-express');
const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub();
const typeDefs = gql`
type IceCream {
id: Int!
flavour: String!
description: String!
}
type Query {
iceCream(flavour: String!): IceCream
iceCreams: [IceCream]
}
type Subscription {
iceCreamDispensed(flavour: String): IceCream
}
`
const stub = [
{
id: 0,
flavour: 'vanilla',
description: 'A flippant pepper bouquet and alcoholic garlic essences are blended in'
},{
id: 1,
flavour: 'strawberry',
description: 'Blends indigestible parsnip flavors with a sandy cool ranch flavor'
},{
id: 2,
flavour: 'pear',
description: 'A soporiphic coconut finish and enticing Bar-B-Q midtones are intertwined'
}
]
const ICE_CREAM_DISPENSED = 'ICE_CREAM_DISPENSED';
const resolvers = {
Subscription: {
iceCreamDispensed: {
subscribe:
withFilter(
() => pubsub.asyncIterator([ ICE_CREAM_DISPENSED ]),
({ iceCreamDispensed: { flavour:payloadFlavour } }, { flavour:requestFlavour }) => {
if (!requestFlavour) {
return true
}
return payloadFlavour === requestFlavour
}
)
}
},
Query: {
iceCreams: () => {
return Promise.resolve(stub)
},
iceCream: (_, { flavour }) => {
return Promise.resolve(stub.find(({ flavour:stubFlavour }) => flavour === stubFlavour))
}
}
};
const server = new ApolloServer({
typeDefs,
resolvers
})
const app = express()
app.post('/dispatch', (req, res) => {
const flavours = ['vanilla','pear','strawberry']
const flavour = flavours[Math.round(Math.random() * (flavours.length - 1))]
const ice = stub.find(item => item.flavour === flavour)
pubsub.publish(ICE_CREAM_DISPENSED, { iceCreamDispensed: ice } )
return res.status(202).send('accepted')
})
server.applyMiddleware({ app })
const httpServer = createServer(app)
server.installSubscriptionHandlers(httpServer)
const port = 3000
httpServer.listen(port, () => {
console.log(`Server ready at http://localhost:${port}${server.graphqlPath}`)
console.log(`Subscriptions ready at ws://localhost:${port}${server.subscriptionsPath}`)
})