Building live chat app with GraphQL subscriptions

Introduction

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:

graphql-realtime-chat-demo

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:

1$ cd graphql-chat-app
2    $ mkdir server

Next, cd into server and run the command below:

1$ cd server
2    $ 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:

1// server/src/index.js
2    
3    const { GraphQLServer, PubSub } = require('graphql-yoga')
4    const typeDefs = require('./schema')
5    const resolvers = require('./resolver')
6    
7    const pubsub = new PubSub()
8    const server = new GraphQLServer({ typeDefs, resolvers, context: { pubsub } })
9    
10    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:

1// server/src/schema.js
2    
3    const typeDefs = `
4      type Chat {
5        id: Int!
6        from: String!
7        message: String!
8      }
9      
10      type Query {
11        chats: [Chat]
12      }
13      
14      type Mutation {
15        sendMessage(from: String!, message: String!): Chat
16      }
17      
18      type Subscription {
19        messageSent: Chat
20      }
21    `
22    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:

1// server/src/resolver.js
2    
3    const chats = []
4    const CHAT_CHANNEL = 'CHAT_CHANNEL'
5    
6    const resolvers = {
7      Query: {
8        chats (root, args, context) {
9          return chats
10        }
11      },
12      
13      Mutation: {
14        sendMessage (root, { from, message }, { pubsub }) {
15          const chat = { id: chats.length + 1, from, message }
16          
17          chats.push(chat)
18          pubsub.publish('CHAT_CHANNEL', { messageSent: chat })
19          
20          return chat
21        }
22      },
23      
24      Subscription: {
25        messageSent: {
26          subscribe: (root, args, { pubsub }) => {
27            return pubsub.asyncIterator(CHAT_CHANNEL)
28          }
29        }
30      }
31    }
32    
33    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:

1$ cd frontend
2    $ 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:

1// frontend/src/main.js
2    
3    import { InMemoryCache } from 'apollo-cache-inmemory'
4    import { ApolloClient } from 'apollo-client'
5    import { split } from 'apollo-link'
6    import { HttpLink } from 'apollo-link-http'
7    import { WebSocketLink } from 'apollo-link-ws'
8    import { getMainDefinition } from 'apollo-utilities'
9    import Vue from 'vue'
10    import VueApollo from 'vue-apollo'
11    import App from './App.vue'
12    
13    Vue.config.productionTip = false
14    
15    const httpLink = new HttpLink({
16      uri: 'http://localhost:4000'
17    })
18    
19    const wsLink = new WebSocketLink({
20      uri: 'ws://localhost:4000',
21      options: {
22        reconnect: true
23      }
24    })
25    
26    const link = split(
27      ({ query }) => {
28        const { kind, operation } = getMainDefinition(query)
29        return kind === 'OperationDefinition' && operation === 'subscription'
30      },
31      wsLink,
32      httpLink
33    )
34    
35    const apolloClient = new ApolloClient({
36      link,
37      cache: new InMemoryCache(),
38      connectToDevTools: true
39    })
40    
41    const apolloProvider = new VueApollo({
42      defaultClient: apolloClient
43    })
44    
45    Vue.use(VueApollo)
46    
47    new Vue({
48      apolloProvider,
49      render: h => h(App)
50    }).$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:

1// frontend/public/index.html
2    
3    <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:

1// frontend/src/App.vue
2    
3    <template>
4      <div id="app" class="container" style="padding-top: 100px">
5        <div class="row justify-content-center">
6          <div class="col-md-8">
7            <div class="card">
8              <div class="card-body">
9                <div class="row" v-if="entered">
10                  <div class="col-md-12">
11                    <div class="card">
12                      <div class="card-header">Chatbox</div>
13                      <div class="card-body">
14                        <!-- messages will be here -->
15                      </div>
16                    </div>
17                  </div>
18                </div>
19                <div class="row" v-else>
20                  <div class="col-md-12">
21                    <form method="post" @submit.prevent="enterChat">
22                      <div class="form-group">
23                        <div class='input-group'>
24                          <input
25                            type='text'
26                            class="form-control"
27                            placeholder="Enter your username"
28                            v-model="username"
29                          >
30                          <div class='input-group-append'>
31                            <button class='btn btn-primary' @click="enterChat">Enter</button>
32                          </div>
33                        </div>
34                      </div>
35                    </form>
36                  </div>
37                </div>
38              </div>
39            </div>
40          </div>
41        </div>
42      </div>
43    </template>
44    
45    <script>
46    export default {
47      name: 'app',
48      data() {
49        return {
50          username: '',
51          message: '',
52          entered: false,
53        };
54      },
55      methods: {
56        enterChat() {
57          this.entered = !!this.username != '';
58        },
59      },
60    };
61    </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.

graphql-realtime-chat-1

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:

1// frontend/src/App.vue
2    
3    <dl
4      v-for="(chat, id) in chats"
5      :key="id"
6    >
7      <dt>{{ chat.from }}</dt>
8      <dd>{{ chat.message }}</dd>
9    </dl>
10    
11    <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:

1// frontend/src/App.vue
2    
3    import { CHATS_QUERY } from '@/graphql';
4    
5    // add this after data declaration
6    apollo: {
7      chats: {
8        query: CHATS_QUERY,
9      },
10    },

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:

1// frontend/src/graphql.js
2    
3    import gql from 'graphql-tag'
4    
5    export const CHATS_QUERY = gql`
6      query ChatsQuery {
7        chats {
8          id
9          from
10          message
11        }
12      }
13    `

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:

1// frontend/src/App.vue
2    
3    <input
4      type='text'
5      class="form-control"
6      placeholder="Type your message..."
7      v-model="message"
8      @keyup.enter="sendMessage"
9    >

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:

1// frontend/src/App.vue
2    
3    import { CHATS_QUERY, SEND_MESSAGE_MUTATION } from '@/graphql';
4    
5    // add these inside methods
6    async sendMessage() {
7      const message = this.message;
8      this.message = '';
9      
10      await this.$apollo.mutate({
11        mutation: SEND_MESSAGE_MUTATION,
12        variables: {
13          from: this.username,
14          message,
15        },
16      });
17    },

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:

1// frontend/src/graphql.js
2    
3    export const SEND_MESSAGE_MUTATION = gql`
4      mutation SendMessageMutation($from: String!, $message: String!) {
5        sendMessage(
6          from: $from,
7          message: $message
8        ) {
9          id
10          from
11          message
12        }
13      }
14    `

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.

graphql-realtime-chat-3

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:

1// frontend/src/graphql.js
2    
3    export const MESSAGE_SENT_SUBSCRIPTION = gql`
4      subscription MessageSentSubscription {
5        messageSent {
6          id
7          from
8          message
9        }
10      }
11    `

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

1// frontend/src/App.vue
2    
3    import {
4      CHATS_QUERY,
5      SEND_MESSAGE_MUTATION,
6      MESSAGE_SENT_SUBSCRIPTION,
7    } from '@/graphql';

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

1// frontend/src/App.vue
2    
3    apollo: {
4      chats: {
5        query: CHATS_QUERY,
6        subscribeToMore: {
7          document: MESSAGE_SENT_SUBSCRIPTION,
8          updateQuery: (previousData, { subscriptionData }) => {
9            return {
10              chats: [...previousData.chats, subscriptionData.data.messageSent],
11            };
12          },
13        },
14      },
15    },

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.

graphql-realtime-chat-demo

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.