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.
This tutorial assumes the following:
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:
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.
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.
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.
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
.
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:
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.
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.
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
.
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.
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.
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.
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.