Building live chat app with GraphQL subscriptions
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.
2 September 2018
by Chimezie Enyinnaya