June 26, 2020

Chat App Development: Create Chat Room App in 20 mins

Sebastian Scholl
@SebScholl

My father likes to remind me that as a computer engineer in the 1970s, “he was a coder before coding was cool.“ Once or twice he even pulled out old school Fortran and COBOL scripts. After reading that code, I feel confident saying that coding today is definitely cooler!

A remarkable trait of modern computer languages and development frameworks is how much less code they allow a developer to write. By leveraging high-level languages alongside the many APIs, open-source packages, and paid services that are available, applications — even ones with complex requirements–can get built incredibly fast.

A helpful comparison for conceptualizing this point in software development’s evolution is to look at construction. Once upon a time, building any house started with cutting down your own trees. However, standard materials, tools, and best practices quickly developed to allow projects to complete faster, structures stand studier, and alleviate workers from lower-level tasks.

How many skyscrapers would there be if to build one you had to mine your own steel?

Engineers who are still young and working started their careers having to cut down trees. That said, unprecedented innovation in the past decade has led to the software industry maturing similarly to that of construction. 

Put simply, today’s developers now have standard tools, materials and best practices that allow projects to complete faster, applications run stable, and alleviate developers from lower-level tasks.

Chat App Creation Overview

Let’s build something in minutes that traditionally would have taken days or weeks; good news, it’s not a To-do’s app! We’re going to build a Public Chat Room application that uses WebSockets to enable real-time messaging. 

WebSockets are natively supported across all modern browsers. However, our goal is to highlight what tools we can bring to the job, not build on the job. In the spirit of that, we’ll use the following technologies.

  • 8base —  A managed GraphQL API
  • VueJS — A front-end JavaScript framework

The starter project and full README file can be found in this GitHub repo. If you only want to review the finished app, checkout the public-chat-room branch.

Additionally, the video below walks through the tutorial and explains each step with greater context.

That said, let’s get going!

Here is the 7 Step Process of How to Build a Chat App:

1. Project Setup

Clone the starter project and move into the group-chat directory. Whether you use {% code-line %}yarn{% code-line-end %} or {% code-line %}npm{% code-line-end %} to install the project dependencies is up to you. Either way, we need all NPM packages declared in the {% code-line %}package.json{% code-line-end %} file.

{% code-block language="shell" %}
# Clone project
git clone https://github.com/8base/Chat-application-using-GraphQL-Subscriptions-and-Vue.git group-chat
# Move into directory
cd group-chat
# Install dependencies
yarn
{% code-block-end %}

In order to communicate with the GraphQL API, there are 3 environment variables we must set. Create a .env.local file in the root directory with the following command. The Vue app will set environment variables we add to this file automatically once initialized.

{% code-block language="shell" %}
echo 'VUE_APP_8BASE_WORKSPACE_ID=<YOUR_8BASE_WORKSPACE_ID>
VUE_APP_8BASE_API_ENDPOINT=https://api.8base.com
VUE_APP_8BASE_WS_ENDPOINT=wss://ws.8base.com' \
> .env.local
{% code-block-end %}

Both the {% code-line %}VUE_APP_8BASE_API_ENDPOINT{% code-line-end %} and {% code-line %}VUE_APP_8BASE_WS_ENDPOINT{% code-line-end %} values are always the same. The value that we'll need to update is the {% code-line %}VUE_APP_8BASE_WORKSPACE_ID{% code-line-end %}. 

If you have an 8base workspace that you want to use for this tutorial, go ahead and update your .env.local file with the workspace ID. Otherwise, retrieve a workspace ID by completing steps 1 & 2 of the 8base Quickstart.

2. Importing the Schema

We now need to provision the backend. At the root of this repo you should find a {% code-line %}chat-schema.json{% code-line-end %} file. To import it to the workspace, we simply need to install and authenticate the 8base command line and then import the schema file.

{% code-block language="shell" %}
# Install 8base CLI
yarn global add 8base-cli
# Authenticate CLI
8base login
# Import the schema to our workspace
8base import -f chat-schema.json -w <YOUR_8BASE_WORKSPACE_ID>
{% code-block-end %}

3. API Permissions

The last backend task is to enable public access to the GraphQL API.

In the 8base Console, navigate to {% code-line %}App Services > Roles > Guest{% code-line-end %}. Update the permissions set for both Messages and Users to be either checked or All Records (as seen in the screenshot below).

The Guest role defines what a user making an unauthenticated request to the API is permitted to do.

8base Permissions editor in Console


4. Writing the GraphQL Queries

At this point, we’re going to define and write out all of the GraphQL queries that we’ll be needing for our chat component. This will help us understand what data we will be reading, creating, and listening to (via WebSockets) using the API.

The following code should be put in the {% code-line %}src/utils/graphql.js{% code-line-end %} file. Read the comments above each exported constant to understand what each query is accomplishing.

{% code-block language="js" %}
/* gql converts the query strings into graphQL documents */
import gql from "graphql-tag";

/* 1. Fetch all online chat Users and last 10 messages */
export const InitialChatData = gql`
{
  usersList {
    items {
      id
      email
    }
  }
  messagesList(last: 10) {
    items {
      content
      createdAt
      author {
        id
        email
      }
    }
  }
}
`;

/* 2. Create new chat users and assign them the Guest role */
export const CreateUser = gql`
mutation($email: String!) {
  userCreate(data: { email: $email, roles: { connect: { name: "Guest" } } }) {
    id
  }
}
`;

/* 3. Delete a chat user */
export const DeleteUser = gql`
mutation($id: ID!) {
  userDelete(data: { id: $id, force: true }) {
    success
  }
}
`;

/* 4. Listen for when chat users are created or deleted */
export const UsersSubscription = gql`
subscription {
  Users(filter: { mutation_in: [create, delete] }) {
    mutation
    node {
      id
      email
    }
  }
}
`;

/* 5. Create new chat messages and connect it to it's author */
export const CreateMessage = gql`
mutation($id: ID!, $content: String!) {
  messageCreate(
    data: { content: $content, author: { connect: { id: $id } } }
  ) {
    id
  }
}
`;

/* 6. Listen for when chat messages are created. */
export const MessagesSubscription = gql`
subscription {
  Messages(filter: { mutation_in: create }) {
    node {
      content
      createdAt
      author {
        id
        email
      }
    }
  }
}
`;
{% code-block-end %}

5. Creating the Apollo and Subscription Client

With our GraphQL queries written it’s time to set up our API modules.

First, let’s tackle the API client using {% code-line %}ApolloClient{% code-line-end %} with its required defaults. To the {% code-line %}createHttpLink{% code-line-end %} we supply our fully formed workspace endpoint. This code belongs at {% code-line %}src/utils/api.js{% code-line-end %}.

{% code-block language="js" %}
import { ApolloClient } from "apollo-boost";
import { createHttpLink } from "apollo-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";

const { VUE_APP_8BASE_API_ENDPOINT, VUE_APP_8BASE_WORKSPACE_ID } = process.env;

export default new ApolloClient({
link: createHttpLink({
  uri: `${VUE_APP_8BASE_API_ENDPOINT}/${VUE_APP_8BASE_WORKSPACE_ID}`,
}),
cache: new InMemoryCache(),
});

// Note: To learn more about the options available when configuring // ApolloClient, please reference their documentation.
{% code-block-end %}

Next, let’s tackle the Subscription client using {% code-line %}subscriptions-transport-ws{% code-line-end %} and {% code-line %}isomorphic-ws{% code-line-end %}. The script is a little longer than our last one, so please take the time to read through the in code comments!

We’re initializing the {% code-line %}SubscriptionClient{% code-line-end %} using our WebSockets endpoint and {% code-line %}workspaceId{% code-line-end %} in the {% code-line %}connectionParams{% code-line-end %}. Then we use that {% code-line %}subscriptionClient{% code-line-end %} in two methods defined on the default export; {% code-line %}subscribe(){% code-line-end %} and {% code-line %}close(){% code-line-end %}.

subscribe allows us to create new subscriptions with data and error callbacks. The close method is what we can use to close the connection when leaving the chat room.

{% code-block language="js" %}
import WebSocket from "isomorphic-ws";
import { SubscriptionClient } from "subscriptions-transport-ws";

const { VUE_APP_8BASE_WS_ENDPOINT, VUE_APP_8BASE_WORKSPACE_ID } = process.env;

/**
* Create the subscription client using the relevant environment
* variables and default options
*/

const subscriptionClient = new SubscriptionClient(
VUE_APP_8BASE_WS_ENDPOINT,
{
  reconnect: true,
  connectionParams: {
    /**
      * WorkspaceID MUST be set or the Websocket Endpoint wont be able to
      * map the request to the appropriate workspace
      */
    workspaceId: VUE_APP_8BASE_WORKSPACE_ID,
  },
},
/**
  * Constructor for W3C compliant WebSocket implementation. Use this when
  * your environment does not have a built-in native WebSocket
  * (for example, with NodeJS client)
  */
WebSocket
);

export default {
/**
  * Accepts the subscription query, any variables and the
  * callback handlers 'data' and 'error'
  */
subscribe: (query, options) => {
  const { variables, data, error } = options;

  /**
    * Runs the new subscription request.
    */
  const result = subscriptionClient.request({
    query,
    variables,
  });

  /**
    * The unsubscribe function can be used to close a specific
    * subscription as opposed to ALL subscriptions be maintained
    * by the subscriptionClient
    */
  const { unsubscribe } = result.subscribe({
    /**
      * Whenever an event is received, the result is passed
      * to the developer specified data callback.
      */
    next(result) {
      if (typeof data === "function") {
        data(result);
      }
    },
    /**
      * Whenever an error is received, the error is passed
      * to the developer specified error callback.
      */
    error(e) {
      if (typeof error === "function") {
        error(e);
      }
    },
  });

  return unsubscribe;
},

/**
  * Closes subscriptionClient's connection.
  */
close: () => {
  subscriptionClient.close();
},
};
// Note: To learn more about the SubscriptionClient and its options, // please reference their documentation
{% code-block-end %}

6. Writing the Vue Component

At this point, we have everything we need to build our public chat room! All that’s left is writing out a single {% code-line %}GroupChat.vue{% code-line-end %} component. 

Boot up the component with yarn serve and let's keep going.

WARNING: Beauty is in the eye of the beholder… and because of that only the minimum styling needed to make the component functional have been added.

Component script

First we need to import our modules, simple styling, and GraphQL queries. All of those artifacts exist in our {% code-line %}src/utils directory{% code-line-end %}. 

Insert the following import statements after the opening {% code-line %}\<script\>{% code-line-end %} tag in {% code-line %}GroupChat.vue{% code-line-end %}.

{% code-block language="js" %}
/* API modules */
import Api from "./utils/api";
import Wss from "./utils/wss";

/* graphQL operations */
import {
InitialChatData,
CreateUser,
DeleteUser,
UsersSubscription,
CreateMessage,
MessagesSubscription,
} from "./utils/graphql";

/* Styles */
import "../assets/styles.css";
{% code-block-end %}

Component data

We can define what data properties we want in our component’s data function. All we need is a way to store chat users, messages, who the "current" user is, and any message that's not yet sent. These properties can be added like so:

{% code-block language="js" %}
/* imports ... */

export default {
name: "GroupChat",

data: () => ({
  messages: [],
  newMessage: "",
  me: { email: "" },
  users: [],
}),
};
{% code-block-end %}

Lifecycle hooks

Our lifecycle hooks execute at different moments in the Vue component’s “life”. For example, when it’s mounted or updated. In our case, we only care about when the component is created and {% code-line %}beforeDestroy{% code-line-end %}. At those times, we want to either open chat subscriptions or close chat subscriptions. 

{% code-block language="js" %}
/* imports ... */

export default {
/* other properties ... */

/**
  * Lifecycle hook executed when the component is created.
  */
created() {
  /**
    * Create Subscription that triggers event when user is created or deleted
    */
  Wss.subscribe(UsersSubscription, {
    data: this.handleUser,
  });
  /**
    * Create Subscription that triggers event when message is created
    */
  Wss.subscribe(MessagesSubscription, {
    data: this.addMessage,
  });
  /**
    * Fetch initial chat data (Users and last 10 Messages)
    */
  Api.query({
    query: InitialChatData,
  }).then(({ data }) => {
    this.users = data.usersList.items;
    this.messages = data.messagesList.items;
  });
  /**
    * Callback executed on page refresh to close chat
    */
  window.onbeforeunload = this.closeChat;
},

/**
  * Lifecycle hook executed before the component is destroyed.
  */
beforeDestroy() {
  this.closeChat();
},
};
{% code-block-end %}

Component methods

We must add specific methods intended to handle each API call/response ({% code-line %}createMessage, addMessage, closeChat, etc.{% code-line-end %}). These will all get stored in the methods object on our component.

One thing worth noting is that most of the mutations do not wait for or handle responses. This is because we have subscriptions running that listen for those mutations. After running successfully, it’s the subscription that handles the event data.

Most of these methods are pretty self explanatory. Regardless, please read the comments in the following code.

{% code-block language="js" %}
/* imports ... */

export default {
/* other properties ... */

methods: {
  /**
    * Create the new user using a submitted email address.
    */
  createUser() {
    Api.mutate({
      mutation: CreateUser,
      variables: {
        email: this.me.email,
      },
    });
  },
  /**
    * Delete a user by their ID.
    */
  deleteUser() {
    Api.mutate({
      mutation: DeleteUser,
      variables: { id: this.me.id },
    });
  },
  /**
    * Our users subscription listing to both the create and update events, and
    * therefore we need to choose the appropriate method to handle the response
    * based on the mutation type.
    *
    * Here, we have an object that looks up the mutation type by name, returns
    * it and executes the function while passing the event node.
    */
  handleUser({
    data: {
      Users: { mutation, node },
    },
  }) {
    ({
      create: this.addUser,
      delete: this.removeUser,
    }[mutation](node));
  },
  /**
    * Adds a new user to users array, first checking whether the user
    * being added is the current user.
    */
  addUser(user) {
    if (this.me.email === user.email) {
      this.me = user;
    }
    this.users.push(user);
  },
  /**
    * Removes user from the users array by ID.
    */
  removeUser(user) {
    this.users = this.users.filter(
      (p) => p.id != user.id
    );
  },
  /* Create a new message */
  createMessage() {
    Api.mutate({
      mutation: CreateMessage,
      variables: {
        id: this.me.id,
        content: this.newMessage,
      },
    }).then(() => (this.newMessage = ""));
  },
  /**
    * Our messages subscription only listens to the create event. Therefore, all we
    * need to do is push it into our messages array.
    */
  addMessage({
    data: {
      Messages: { node },
    },
  }) {
    this.messages.push(node);
  },
  /**
    * We'll want to close our subscriptions and delete the user. This method can be
    * called in our beforeDestroy lifecycle hook and any other relevantly placed callback.
    */
  closeChat () {
    /* Close subscriptions before exit */
    Wss.close()
    /* Delete participant */
    this.deleteUser();
    /* Set me to default */
    this.me = { me: { email: '' } }
  }
},

/* lifecycle hooks ... */
}
{% code-block-end %}

Component template

Last but not least, we have our component {% code-line %}\<template\>{% code-line-end %}. 

There are 1000's of great tutorials out there on how to build beautiful UIs. This is not one of those tutorials.

The following template meets the minimum spec of a group chat app. It’s really up to you to go in and make it beautiful. That said, let’s quickly walk through the key markup that we’ve implemented here. 

As always, please read the in-line code comments.

{% code-block language="html" %}
<template>
<div id="app">
  <!--
    Only if the current user has an ID (has been created) should the
    chat view be rendered. Otherwise, a sign up for is shown.
    -->
  <div v-if="me.id" class="chat">
    <div class="header">
      <!--
        Since we're using subscriptions that run in real-time, our
        number of user currently online will dynamically adjust.
        -->
      {{ users.length }} Online Users
      <!--
        A user can leave the chat by executing the
        closeChat function.
        -->
      <button @click="closeChat">Leave Chat</button>
    </div>
    <!--
      For every message that we're storing in the messages array,
      we'll render out in a div. Additionally, if the messages participant
      id matches the current user id, we'll assign it the me class.
      -->
    <div
      :key="index"
      v-for="(msg, index) in messages"
      :class="['msg', { me: msg.participant.id === me.id }]"
    >
      <p>{{ msg.content }}</p>
      <small
        ><strong>{{ msg.participant.email }}</strong> {{ msg.createdAt
        }}</small
      >
    </div>
    <!--
      Our message input is bound to the newMessage data property.
      -->
    <div class="input">
      <input
        type="text"
        placeholder="Say something..."
        v-model="newMessage"
      />
      <!--
        When the user clicks the send button, we run the createMessage function.
        -->
      <button @click="createMessage">Send</button>
    </div>
  </div>
  <!--
    The sign up flow simply asks the user to enter an email address. Once the
    input is blurred, the createUser method is executed.
    -->
  <div v-else class="signup">
    <label for="email">Sign up to chat!</label>
    <br />
    <input
      type="text"
      v-model="me.email"
      placeholder="What's your email?"
      @blur="createUser"
      required
    />
  </div>
</div>
</template>
{% code-block-end %}

Believe it or not, the whole Public Chat Room is now built. If you open it on your localhost network, you’ll be able to start sending and receiving messages. However, to prove that it’s a real group chat, open several windows and watch the conversation flow!


7. Wrap up and Testing

In this tutorial, we explored how leveraging modern development tools allows us to build real world applications in minutes. 

Hopefully, you also learned how to initialize {% code-line %}ApolloClient{% code-line-end %} and {% code-line %}SubscriptionClient{% code-line-end %} to effectively execute GraphQL queries, mutations, and subscriptions to an 8base workspace, as well as a little bit about VueJS.

Whether you’re working on a web/mobile game, messaging and notification apps, or other projects that have real-time data requirements, subscriptions are an amazing tool to leverage. We barely scratched the surface here!

Build A Chat App with 8base

8base is a production-ready serverless backend-as-a-service built by developers for developers. The 8base platform lets developers build amazing cloud applications using JavaScript and GraphQL. Learn more about the 8base platform.

8base For Non-Developers

8base offers custom software development services. With an easy to use development platform and a team of highly-skilled designers and developers behind you, we can help build your web or mobile based chat application. Learn more about our web and mobile app development services


Ready to try 8base?

We're excited about helping you achieve amazing results.