Levvel Blog - A Guide to Subscriptions in GraphQL with Apollo

A Guide to Subscriptions in GraphQL with Apollo

Intro

In this article, we will implement a small application to create real-time messages and update the favorite ones by using subscriptions with GraphQL, Apollo Server, Apollo Client, and React.

What Are Subscriptions and Where Can They Be Used?

Subscriptions are GraphQL operations to watch events emitted by a server via events. Subscriptions use WebSockets, which means there is an open connection between the server and the client where information is passed back and forth without a specific request-response.

You might want to add subscriptions to your application if you need real-time information. In order to achieve that, you need to open listeners in the back-end to specific events in the front-end.

GraphQL Subscriptions in the Back-end with Apollo-Server 2.0

In this blog post, we will focus on configuring and implementing subscriptions; however, we need to mention that we are using sequelizer as our ORM (object-relational mapping) to talk to the database on MySql. You can see the configuration of that part of the backend in the itr-apollo-server GitHub repo.

Packages

First, we need to install the necessary packages:

apollo-server apollo-server-express express graphql graphql-tools

npm install --save apollo-server apollo-server-express express graphql graphql-tools

Schema

Second, we need to configure our Apollo Server to listen to subscriptions by defining our models, queries, mutations, and subscriptions.

Subscriptions are another root level type so they need to be defined in our schema with a name and a return type similar to the mutations and queries.

import { gql } from 'apollo-server-express';

const typeDefs = gql`
   type Message {
       id: Int!,
       text: String!,
       isFavorite: Boolean!
   }
   type Query {
       allMessages: [Message]
       fetchMessage(id: Int!): Message
   }
   type Mutation {
       createMessage (
           text: String!
       ): Message
       updateMessage (
           id: Int!
           text: String!
           isFavorite: Boolean!
       ): Message
   },
   type Subscription {
       messageCreated: Message
       messageUpdated(id: Int!): Message
   }
`;

module.exports = typeDefs;

Resolvers

After defining our schema, we need to set our resolvers. In order to configure the subscriptions we require PubSub which comes from the terms publish and subscribe; its purpose is to create event generators.

Note: PubSub comes out of the box in Apollo Server for demo purposes; however, it is advisable a replacement by any other implementation of PubSub engine interface in order to scale in production.

In this example, we are adding a subscription when we create and update a message. Notice that Subscription resolvers are objects (not functions) with a Subscribe method that returns an AsyncIterator to map with the desired event.

import { PubSub, withFilter } from 'apollo-server';
import { Message } from '../models';
require('dotenv').config();

const MESSAGE_CREATED = 'MESSAGE_CREATED';
const MESSAGE_UPDATED = 'MESSAGE_UPDATED';

const pubsub = new PubSub();

const resolvers = {
    Query: {
        async allMessages() {
            return await Message.all({order:[['id', 'DESC']]});
        },
        async fetchMessage(_, { id }) {
            return await Message.findById(id);
        },
    },
    Mutation: {
        async createMessage(_, { text }) {
            const message = await Message.create({ text });
            await pubsub.publish(MESSAGE_CREATED, { messageCreated: message });
            return message;
        },
        async updateMessage(_, { id, text, isFavorite}) {
            const message = await Message.findById(id);
            await message.update({text,isFavorite})
            .then(message=>{
                pubsub.publish(MESSAGE_UPDATED, { messageUpdated: message });
            });
            return message;
        },
    },
    Subscription: {
        messageCreated: {
          subscribe: () => pubsub.asyncIterator([MESSAGE_CREATED]),
        },
        messageUpdated: {
            subscribe: withFilter(
                                  () => pubsub.asyncIterator('MESSAGE_UPDATED'),
                                        (payload, variables) => {
                                                return payload.messageUpdated.id === variables.id;
                                            },
                                  ),
          },
    }
}

export default resolvers;

Publish

In our createMessage mutation, we are adding an extra step after the message is created; that step is where we are publishing a MESSAGE_CREATED event and sending an object of type Message.

async createMessage(_, { text }) {
            const message = await Message.create({ text });
            await pubsub.publish(MESSAGE_CREATED, { messageCreated: message });
            return message;
        },

In a similar way, we publish a MESSAGE_UPDATED on the updateMessage mutation.

async updateMessage(_, { id, text, isFavorite}) {
            const message = await Message.findById(id);
            await message.update({text,isFavorite})
            .then(message=>{
                pubsub.publish(MESSAGE_UPDATED, { messageUpdated: message });
            });
            return message;
        },

Those steps are important because we are telling the server to trigger a certain event, and the front-end will be listening for those events. In this case: MESSAGE_CREATED and MESSAGE_UPDATED.

Subscribe

In the Subscription section of our resolver, we will subscribe to the event we need via the asyncIterator. For example:

messageCreated: {
          subscribe: () => pubsub.asyncIterator([MESSAGE_CREATED]),
        },

For the MessageUpdated subscription, we will apply a filter in order to control the publication for a specific message. We just want to publish the message that was updated based on the ID.

messageUpdated: {
            subscribe: withFilter(
                                  () => pubsub.asyncIterator('MESSAGE_UPDATED'),
                                        (payload, variables) => {
                                                return payload.messageUpdated.id === variables.id;
                                            },
                                  ),
          }

Notice that when using withFilter we do not need to wrap the return with a function.

Applying Middleware to Our Server

The last step on the server side is to use installSubscriptionHandlers as a middleware to our Apollo Server in order to use subscriptions.

import express from 'express';
import { createServer } from 'http';
import { ApolloServer } from 'apollo-server-express';

import typeDefs  from './data/schema';
import resolvers from './data/resolvers';

const PORT = process.env.PORT ||  4000;

const app = express();

const apolloServer = new ApolloServer({ typeDefs, resolvers });
apolloServer.applyMiddleware({ app });

const httpServer = createServer(app);
apolloServer.installSubscriptionHandlers(httpServer);

httpServer.listen({ port: PORT }, () =>{
  console.log(`🚀 Server ready at http://localhost:${PORT}${apolloServer.graphqlPath}`)
  console.log(`🚀 Subscriptions ready at ws://localhost:${PORT}${apolloServer.subscriptionsPath}`)
})

Before we move on to the front end, it is a good practice to test that our mutations and subscriptions are running as expected in the back end, that will help us to avoid errors further along our implementation on the front-end.

Testing subscriptions

When running subscriptions on the playground we should just see the play button changing from Play to Stop. This means we opened a listener—just when a subscription is triggered, a result will be displayed. Otherwise, do not expect to see anything.

Testing Mutations

You can find the source code for the back-end in the following GitHub repo: itr-apollo-server. Let’s move on to the front-end implementation.

GraphQL Subscriptions in the Front-end with Apollo-Client 2.0

We will create the well known create-react-app and add some components and styles to set our basic UI. You can see that boilerplate in the itr-apollo-client GitHub repo.

Focusing on the subscriptions, the first step is to add the WebSocketLink via the ApolloLink middleware, so we need to install: apollo-link apollo-link-ws

npm install –save apollo-link apollo-link-ws

The purpose of those packages is to provide a way to direct to different links depending on the operation, so make sure you add the corresponding imports in your index.js file on your front-end application. We also need to add the ApolloProvider and wrap our App on it.

Your index.js file should look like the following:

import React from 'react';
import ReactDOM from 'react-dom';
import { ApolloProvider } from 'react-apollo';
import { ApolloClient } from 'apollo-client';
import { getMainDefinition } from 'apollo-utilities';
import { ApolloLink, split } from 'apollo-link';
import { HttpLink } from 'apollo-link-http';
import { WebSocketLink } from 'apollo-link-ws';
import { InMemoryCache } from 'apollo-cache-inmemory';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

const httpLink = new HttpLink({
    uri: 'http://localhost:4000/graphql',
});

const wsLink = new WebSocketLink({
    uri: `ws://localhost:4000/graphql`,
    options: {
      reconnect: true,
    },
});

const terminatingLink = split(
    ({ query }) => {
      const { kind, operation } = getMainDefinition(query);
      return (
        kind === 'OperationDefinition' && operation === 'subscription'
      );
    },
    wsLink,
    httpLink,
);

const link = ApolloLink.from([terminatingLink]);

const cache = new InMemoryCache();

const client = new ApolloClient({
    link,
    cache,
});


ReactDOM.render(<ApolloProvider client={client}>
                    <App />
                </ApolloProvider>,
                document.getElementById('root'));

registerServiceWorker();

Usually a normal HttpLink is enough to set the Apollo Client; however, as we need to get access to the WebSocket we need to create an instance of a WebSocketLink so we know the endpoint.

Split allows you to control the flow of the operations. For example, if it is a mutation it will go via the HttpLink and if it is a subscription it will go to via the WebSocketLink; this is called Directional Composition.

const terminatingLink = split(
    ({ query }) => {
      const { kind, operation } = getMainDefinition(query);
      return (
        kind === 'OperationDefinition' && operation === 'subscription'
      );
    },
    wsLink,
    httpLink,
);

const link = ApolloLink.from([terminatingLink]);

Once we have our Apollo provider configured, let’s implement our two subscriptions: messageCreated and messageUpdated in our components.

We will create a component (message) where the messages will be created. For simplicity, I will focus on the two most important elements: the graphql-tag and the trigger event.

onSend=()=>{
        //call the creation mutation
        this.props.mutate({
            variables: { text: this.state.message }
          })
        //clean the text input
        this.setState({message:""});
}


....


const CREATE_MESSAGE = gql`
 mutation createMessage($text: String!){
      createMessage(text:$text){
            id,
            text    
  }        
}`;

export default graphql(CREATE_MESSAGE)(Message);

Now, let’s create a Query component that retrieves the list of messages. The render prop function passed to the query component has some other properties, like subscribeToMore, which is a function that we will use to set up the subscription, hence why we will pass it as a property to our list of messages.

First, we need to install npm install --save react-apollo

const MessagesContainer = () => (
    <Query query={GET_MESSAGES}>
      {({ data, loading, error, subscribeToMore }) => {
        if (!data) {
          return null;
        }
        if (loading) {
          return <span>Loading ...</span>;
        }
        if (error) { 
          return <p>Sorry! Something went wrong.</p>;
        }

        return (<MessageList
            messages={data.allMessages}
            subscribeToMore={subscribeToMore}
          />);
      }}
    </Query>
  );

As far as the graphql query to retrieve the list of messages, I recommend copying the same query you had in the playground on the back-end testing section.

messageCreated

We will add our first subscription (messageCreated) in the componentDidMount method of our list of messages component.

We need to pass the following properties: document, which is the name of our document passed by the graphql-tag updateQuery, which is the function that will be executed once we are notified that an event happened. In this case, we will add the message created (subscriptionData.data.messageCreated) at the top of our current list of messages (…prev.allMessages).

    componentDidMount() {
      this.props.subscribeToMore({
        document: MESSAGE_CREATED,
        updateQuery: (prev, { subscriptionData }) => {
          if (!subscriptionData.data) return prev;
          return {
            allMessages: [
              subscriptionData.data.messageCreated,
              ...prev.allMessages
            ],
          };
        },
      });
    }

messageUpdated

To you show another way to create a subscription, we are going to use the component from the react-apollo package that we already installed. The first step is to create the graphql tag. Remember to use the code you used to test your back-end in order to avoid errors.

The second step is to add the subscription component in our render method and pass the following properties:

subscription, which is the name of our graphql tag variables (if any)

In the children section, we can render some specific component or information based on the response of the subscription, or simply return null.

    render() {
      return (<div>
          {this.props.messages.map(message => {
          return (<div key={message.id}>
            <MessageElement message={message}/>
            <Subscription subscription={MESSAGE_UPDATED}
                            variables={{ id: message.id }}>
                            {() => {return null;}}
              </Subscription>
          </div>)
          })}
        </div>
      );
    }

Notice that we can nest subscriptions inside a query or a mutation; however, this could cause performance issues as the component gets updated and re-rendered every time.

Our application is ready to emit events and trigger subscriptions; make sure your server is running (in this case http://localhost:4000/ ) and open your browser on http://localhost:3000/. If you add a message the list, it should add it automatically at the top, and if you favorite one message and open two tabs, the change should be reflected in real time on both tabs.

You can find the GitHub repositories in: itr-apollo-server and itr-apollo-client.

Brenda Jimenez

Brenda Jimenez

Application Developer

Brenda has over 8 years of experience in software development among industries like Civil Engineering, Travel, and Fintech. Her professional experience has being a mix of back- and front-end development. However, she has lately been focusing on the front end as she enjoys experimenting with new technologies and likes interacting directly with the customer.

Related Posts