🎉 New release for Pusher Chatkit - Webhooks! Extend your in-app chat functionality
Hide

Building live chat app with GraphQL subscriptions

  • Chimezie Enyinnaya
September 2nd, 2018
You will need Node and the Vue CLI installed on your machine.

One of the exciting things about GraphQL is the ability to build realtime applications with it, through the use of GraphQL subscriptions. In this tutorial, I’ll be showing you how to build a realtime app with GraphQL subscriptions.

Prerequisites

This tutorial assumes the following:

  • Node.js and NPM installed on your computer
  • Vue CLI 3 installed on your computer
  • Basic knowledge of GraphQL
  • Basic knowledge of JavaScript and Vue.js

What we'll be building

We’ll be building a simple chat app. We’ll start by building the GraphQL server, then we’ll build a Vue.js app that will consume the GraphQL server. To keep this tutorial focused, we won’t be working with a database. Instead, we’ll save the chats in an in-memory array.

Below is a quick demo of the final app:

What are GraphQL subscriptions?

Before we dive into code, let’s take a quick look at what is GraphQL subscriptions. GraphQL subscriptions add realtime functionality to GraphQL. They allow a server to send data to clients when a specific event occurs. Just as queries, subscriptions can also have a set of fields, which will be returned to the client. Unlike queries, subscriptions doesn’t immediately return a response, but instead, a response is returned every time a specific event occurs and the subscribed clients will be notified accordingly.

Usually, subscriptions are implemented with WebSockets. You can check out the Apollo GraphQL subscriptions docs to learn more.

Building the GraphQL server

To speed the development process of our GraphQL server, we’ll be using graphql-yoga. Under the hood, graphql-yoga makes use of Express and Apollo Server. Also, it comes bundled with all the things we’ll be needing in this tutorial, such as graphql-subscriptions. So let’s get started.

We’ll start by creating a new project directory, which we’ll call graphql-chat-app:

    $ mkdir graphql-chat-app

Next, let’s cd into the new project directory and create a server directory:

    $ cd graphql-chat-app
    $ mkdir server

Next, cd into server and run the command below:

    $ cd server
    $ npm init -y

Now, let’s install graphql-yoga:

    $ npm install graphql-yoga

Once that’s done installing, we’ll create a src directory inside the server directory:

    $ mkdir src

The src directory is where our GraphQL server code will reside. So let’s create an index.js file inside the src directory and paste the code below in it:

    // server/src/index.js

    const { GraphQLServer, PubSub } = require('graphql-yoga')
    const typeDefs = require('./schema')
    const resolvers = require('./resolver')

    const pubsub = new PubSub()
    const server = new GraphQLServer({ typeDefs, resolvers, context: { pubsub } })

    server.start(() => console.log('Server is running on localhost:4000'))

Here, we import GraphQLServer and PubSub (which will be used to publish/subscribe to channels) from graphql-yoga. Also, we import our schemas and resolvers (which we’ll create shortly). Then we create an instance of PubSub. Using GraphQLServer, we create our GraphQL server passing to it the schemas, resolvers and a context. Noticed we pass pubsub as a context to our GraphQL server. That way, we’ll be able to access it in our resolvers. Finally, we start the server.

Defining the schemas

Inside the src directory, create a schema.js file and paste the code below in it:

    // server/src/schema.js

    const typeDefs = `
      type Chat {
        id: Int!
        from: String!
        message: String!
      }

      type Query {
        chats: [Chat]
      }

      type Mutation {
        sendMessage(from: String!, message: String!): Chat
      }

      type Subscription {
        messageSent: Chat
      }
    `
    module.exports = typeDefs

We start by defining a simple Chat type, which has three fields: the chat ID, the username of the user sending the message and the message itself. Then we define a query to fetch all messages and a mutation for sending a new message, which accepts the username and the message. Lastly, we define a subscription, which we are calling messageSent and it will return a message.

Writing the resolver functions

With the schemas defined, let’s move on to defining the resolver functions. Inside the src directory, create a resolver.js file and paste the code below in it:

    // server/src/resolver.js

    const chats = []
    const CHAT_CHANNEL = 'CHAT_CHANNEL'

    const resolvers = {
      Query: {
        chats (root, args, context) {
          return chats
        }
      },

      Mutation: {
        sendMessage (root, { from, message }, { pubsub }) {
          const chat = { id: chats.length + 1, from, message }

          chats.push(chat)
          pubsub.publish('CHAT_CHANNEL', { messageSent: chat })

          return chat
        }
      },

      Subscription: {
        messageSent: {
          subscribe: (root, args, { pubsub }) => {
            return pubsub.asyncIterator(CHAT_CHANNEL)
          }
        }
      }
    }

    module.exports = resolvers

We create an empty chats array, then we define our channel name, which we call CHAT_CHANNEL. Next, we begin writing the resolver functions. First, we define the function to fetch all the messages, which simply returns the chats array. Then we define the sendMessage mutation. In the sendMessage(), we create a chat object from the supplied arguments and add the new message to the chats array. Next, we make use of the publish() from the pubsub object, which accepts two arguments: the channel (CHAT_CHANNEL) to publish to and an object containing the event (messageSent, which must match the name of our subscription) to be fired and the data (in this case the new message) to pass along with it. Finally, we return the new chat.

Lastly, we define the subscription resolver function. Inside the messageSent object, we define a subscribe function, which subscribes to the CHAT_CHANNEL channel, listens for when the messageSent event is fired and returns the data that was passed along with the event, all using the asyncIterator() from the pubsub object.

Let’s start the server since we’ll be using it in the subsequent sections:

    $ node src/index.js

The server should be running at http://localhost:4000.

Building the frontend app

With the GraphQL server ready, let’s start building the frontend app. Using the Vue CLI, create a new Vue.js app directly inside the project’s root directory:

    $ vue create frontend

At the prompt, we’ll choose the default (babel, eslint) preset.

Once that’s done, let’s install the necessary dependencies for our app:

    $ cd frontend
    $ npm install vue-apollo graphql apollo-client apollo-link apollo-link-http apollo-cache-inmemory graphql-tag apollo-link-ws apollo-utilities subscriptions-transport-ws

That’s a lot of dependencies, so let’s go over each of them:

  • vue-apollo: an Apollo/GraphQL integration for Vue.js.
  • graphql: a reference implementation of GraphQL for JavaScript.
  • apollo-client: a fully-featured, production-ready caching GraphQL client for every server or UI framework.
  • apollo-link: a standard interface for modifying control flow of GraphQL requests and fetching GraphQL results.
  • apollo-link-http: used to get GraphQL results over a network using HTTP fetch.
  • apollo-cache-inmemory: cache implementation for Apollo Client 2.0.
  • graphql-tag: a JavaScript template literal tag that parses GraphQL queries.
  • apollo-link-ws: allows sending of GraphQL operations over a WebSocket.
  • apollo-utilities: utilities for working with GraphQL ASTs.
  • subscriptions-transport-ws: a WebSocket client + server for GraphQL subscriptions.

Next, let’s set up the Vue Apollo plugin. Open frontend/src/main.js and update it as below:

    // frontend/src/main.js

    import { InMemoryCache } from 'apollo-cache-inmemory'
    import { ApolloClient } from 'apollo-client'
    import { split } from 'apollo-link'
    import { HttpLink } from 'apollo-link-http'
    import { WebSocketLink } from 'apollo-link-ws'
    import { getMainDefinition } from 'apollo-utilities'
    import Vue from 'vue'
    import VueApollo from 'vue-apollo'
    import App from './App.vue'

    Vue.config.productionTip = false

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

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

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

    const apolloClient = new ApolloClient({
      link,
      cache: new InMemoryCache(),
      connectToDevTools: true
    })

    const apolloProvider = new VueApollo({
      defaultClient: apolloClient
    })

    Vue.use(VueApollo)

    new Vue({
      apolloProvider,
      render: h => h(App)
    }).$mount('#app')

Here, we create new instances of both httpLink and WebSocketLink with the URLs (http://localhost:4000 and ws://localhost:4000) of our GraphQL server respectively. Since we can have two different types of operations (query/mutation and subscription), we need to configure Vue Apollo to handle both of them. We can easily do that using the split(). Next, we create an Apollo client using the link created above and specify we want an in-memory cache. Then we install the Vue Apollo plugin, and we create a new instance of the Vue Apollo plugin using the apolloClient created as our default client. Lastly, we make use of the apolloProvider object by adding it to our Vue instance.

Adding Bootstrap

For quick prototyping of our app, we’ll be using Bootstrap. So add the line below to the head section of public/index.html:

    // frontend/public/index.html

    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">

For the purpose of this tutorial, we’ll be making use of just one component for everything, that is, the App component.

Joining chat

Since we won’t be covering user authentication in this tutorial, we need a way to get the users in the chat. For that, we’ll ask the user to enter a username before joining the chat. Update frontend/src/App.vue as below:

    // frontend/src/App.vue

    <template>
      <div id="app" class="container" style="padding-top: 100px">
        <div class="row justify-content-center">
          <div class="col-md-8">
            <div class="card">
              <div class="card-body">
                <div class="row" v-if="entered">
                  <div class="col-md-12">
                    <div class="card">
                      <div class="card-header">Chatbox</div>
                      <div class="card-body">
                        <!-- messages will be here -->
                      </div>
                    </div>
                  </div>
                </div>
                <div class="row" v-else>
                  <div class="col-md-12">
                    <form method="post" @submit.prevent="enterChat">
                      <div class="form-group">
                        <div class='input-group'>
                          <input
                            type='text'
                            class="form-control"
                            placeholder="Enter your username"
                            v-model="username"
                          >
                          <div class='input-group-append'>
                            <button class='btn btn-primary' @click="enterChat">Enter</button>
                          </div>
                        </div>
                      </div>
                    </form>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </template>

    <script>
    export default {
      name: 'app',
      data() {
        return {
          username: '',
          message: '',
          entered: false,
        };
      },
      methods: {
        enterChat() {
          this.entered = !!this.username != '';
        },
      },
    };
    </script>

We display a form for entering a username. Once the form is submitted, we call enterChat(), which simply updates the entered data depending on whether the user entered a username or not. Notice we have conditional rendering in the template section. The chat interface will only be rendered when a user has supplied a username. Otherwise, the join chat form will be rendered.

Let’s start the app to see our progress thus far:

    $ npm run serve

The app should be running at http://localhost:8080.

Displaying all chats

Now, let’s display all messages. First, let’s update the template. Replace the messages will be here ****comment with the following:

    // frontend/src/App.vue

    <dl
      v-for="(chat, id) in chats"
      :key="id"
    >
      <dt>{{ chat.from }}</dt>
      <dd>{{ chat.message }}</dd>
    </dl>

    <hr>

Here, we are looping through all the messages (which will be populated from our GraphQL server) and displaying each of them.

Next, add the following to the script section:

    // frontend/src/App.vue

    import { CHATS_QUERY } from '@/graphql';

    // add this after data declaration
    apollo: {
      chats: {
        query: CHATS_QUERY,
      },
    },

We add a new apollo object, then within the apollo object, we define the GraphQL query to fetch all messages. This makes use of the CHATS_QUERY query (which we’ll create shortly).

Next, let’s create the CHATS_QUERY query. Create a new graphql.js file inside frontend/src and paste the following content in it:

    // frontend/src/graphql.js

    import gql from 'graphql-tag'

    export const CHATS_QUERY = gql`
      query ChatsQuery {
        chats {
          id
          from
          message
        }
      }
    `

First, we import graphql-tag. Then we define the query for fetching all chats from our GraphQL server.

Let’s test this. Enter a username to join the chat. For now, the chatbox is empty obviously because we haven’t sent any messages yet.

Send a new message

Let’s start sending messages. Add the code below immediately after the hr tag in the template:

    // frontend/src/App.vue

    <input
      type='text'
      class="form-control"
      placeholder="Type your message..."
      v-model="message"
      @keyup.enter="sendMessage"
    >

We have an input field for entering a new message, which is bound to the message data. The new message will be submitted once we press enter key, which will call a sendMessage().

Next, add the following to the script section:

    // frontend/src/App.vue

    import { CHATS_QUERY, SEND_MESSAGE_MUTATION } from '@/graphql';

    // add these inside methods
    async sendMessage() {
      const message = this.message;
      this.message = '';

      await this.$apollo.mutate({
        mutation: SEND_MESSAGE_MUTATION,
        variables: {
          from: this.username,
          message,
        },
      });
    },

We define the sendMessage(), which makes use of the mutate() available on this.$apollo (from the Vue Apollo plugin). We use the SEND_MESSAGE_MUTATION mutation (which we’ll create shortly) and pass along the necessary arguments (username and message).

Next, let’s create the SEND_MESSAGE_MUTATION mutation. Add the code below inside frontend/src/graphql.js:

    // frontend/src/graphql.js

    export const SEND_MESSAGE_MUTATION = gql`
      mutation SendMessageMutation($from: String!, $message: String!) {
        sendMessage(
          from: $from,
          message: $message
        ) {
          id
          from
          message
        }
      }
    `

Now, if we try sending a message, we and the user we are chatting with won’t see the message until the page is refreshed.

Displaying new messages in realtime

To resolve the issue above, we’ll add realtime functionality to our app. Let’s start by defining the subscription. Add the code below inside frontend/src/graphql.js:

    // frontend/src/graphql.js

    export const MESSAGE_SENT_SUBSCRIPTION = gql`
      subscription MessageSentSubscription {
        messageSent {
          id
          from
          message
        }
      }
    `

Next, in the App component, we also import the MESSAGE_SENT_SUBSCRIPTION subscription we just created.

    // frontend/src/App.vue

    import {
      CHATS_QUERY,
      SEND_MESSAGE_MUTATION,
      MESSAGE_SENT_SUBSCRIPTION,
    } from '@/graphql';

Next, we’ll update the query for fetching all messages as below:

    // frontend/src/App.vue

    apollo: {
      chats: {
        query: CHATS_QUERY,
        subscribeToMore: {
          document: MESSAGE_SENT_SUBSCRIPTION,
          updateQuery: (previousData, { subscriptionData }) => {
            return {
              chats: [...previousData.chats, subscriptionData.data.messageSent],
            };
          },
        },
      },
    },

In addition to just fetching the messages, we now define a subscribeToMore object, which contains our subscription. To update the messages in realtime, we define a updateQuery, which accepts the previous chats data and the data that was passed along with the subscription. So all we have to do is merge the new data to the existing one and return them as the updated messages.

Now, if we test it out, we should see our messages in realtime.

Conclusion

In this tutorial, we have seen how to build realtime apps with GraphQL subscriptions. We started by first building a GraphQL server, then a Vue.js app that consumes the GraphQL server.

The complete code for this tutorial is available on GitHub.

Clone the project repository
  • JavaScript
  • Vue.js
  • GraphQL
  • no pusher tech

Products

  • Channels
  • Chatkit
  • Beams

© 2019 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.