Top of page

Blog

Ice cream with GraphQL subscriptions

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.

Here is the finished code

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}`)
})
← All blog posts

Also in love with the web?

For us, that’s about technology and user experience. Fast, available for all, enjoyable to use. And fun to build. This is how our team bands together, adhering to the same values, to make sure we achieve a solid result for clients both large and small. Does that fit you?

Join our team