Build a group chat app in JavaScript
A basic understanding of JavaScript is needed to follow this tutorial.
In this tutorial, we’re going to build a group chat app in JavaScript and Pusher. Though the app will be basic, it will have some of the features a typical group chat app would have, such as the number of users in the group, notifications when a new user joins the group and finally, display chat messages to all users in the group in real-time. We’ll be using Node.js (Express) as the application server, Vue.js on the front-end and Pusher for realtime communication between our server and frontend.
This tutorial assumes you already have Node.js and NPM installed on your computer.
Let’s take a quick look at what we’ll be building:
Let’s get started!
Create a Pusher app
If you don’t have one already, create a free Pusher account here then log in to your dashboard and create an app. Take note of your app credentials as we’ll be using them shortly. You can find your app credentials under the Keys section on the Overview tab.
Create Node.js server
Having created our Pusher app and gotten our app credentials, we can move on to creating the Node.js server. As stated above, we’ll be using Express as our Node.js framework. Initialize a new Node.js project by using the following command:
mkdir pusher-group-chat
cd pusher-group-chat
npm init -y
Next, we’ll install Express, Pusher’s Node.js package and some other dependencies that our app will need:
npm install express body-parser express-session dotenv pusher --save
You will noticed we installed the dotenv
package which will allow us to pull details from an environment variable from a .env
file.
The rest of this tutorial assumes you’re already cd
to the pusher-group-chat
directory as subsequent commands will be run from within that directory.
Now let’s start fleshing the server, run command to create a new file name server.js
:
touch server.js
Open the newly created file and paste the code below into it:
// server.js
require('dotenv').config();
const express = require('express');
const path = require('path');
const bodyParser = require('body-parser');
const session = require('express-session');
const Pusher = require('pusher');
const app = express();
// Session middleware
app.use(session({
secret: 'somesuperdupersecret',
resave: true,
saveUninitialized: true
}))
// Body parser middleware
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
// Serving static files
app.use(express.static(path.join(__dirname, 'public')));
// Create an instance of Pusher
const pusher = new Pusher({
appId: process.env.PUSHER_APP_ID,
key: process.env.PUSHER_APP_KEY,
secret: process.env.PUSHER_APP_SECRET,
cluster: process.env.PUSHER_APP_CLUSTER,
encrypted: true
});
app.listen(3000, () => {
console.log('Server is up on 3000')
});
First, we create a basic Express server and enable some middleware that the server will use. The session
middleware will allow us to save a user’s details in session for later use. The body-parser
middleware will allow us get form data. Then we serve static files from public
directory.
Next, we create an instance of Pusher. As you can see, we’re loading our Pusher app credentials from environment variables from a .env
(which we’re yet to create) file. Lastly, we start the server on port 3000
.
Before we move to the other part of the server, let’s create the .env
file we talked above:
touch .env
And paste the lines below into it:
// .env
PUSHER_APP_ID=xxxxxx
PUSHER_APP_KEY=xxxxxxxxxxxxxxxxxxxx
PUSHER_APP_SECRET=xxxxxxxxxxxxxxxxxxxx
PUSHER_APP_CLUSTER=xxxx
Remember to replace the xs with your Pusher app credentials.
Adding Routes
Now let’s add the necessary routes our server will need. Open server.js
and add the following code to it just after where we created an instance of Pusher:
// server.js
...
app.get('/', (req, res) => {
res.sendFile('index.html');
});
app.post('/join-chat', (req, res) => {
// store username in session
req.session.username = req.body.username;
res.json('Joined');
});
app.post('/pusher/auth', (req, res) => {
const socketId = req.body.socket_id;
const channel = req.body.channel_name;
// Retrieve username from session and use as presence channel user_id
const presenceData = {
user_id: req.session.username
};
const auth = pusher.authenticate(socketId, channel, presenceData);
res.send(auth);
});
app.post('/send-message', (req, res) => {
pusher.trigger('presence-groupChat', 'message_sent', {
username: req.body.username,
message: req.body.message
});
res.send('Message sent');
});
...
In the snippet above, the first route will render the index.html
which is the main entry point of our app when we hit the /
endpoint. Next, we define the /join-chat
endpoint which will be called when a user fills the input field to join the chat. We get the username entered by the user and save it in the session. Because we’ll be using a presence channel for our group chat app, we need to implement an auth API which will authenticate the user request on the server side. So the /pusher/auth
endpoint will be called automatically when a user subscribes to our chat. We pass along the user’s username retrieved from the session to Pusher’s authenticate
method so it will be available on the client side when a user has subscribed to the channel. Lastly, we create the /send-message
route that uses Pusher to broadcast a message_sent
event to the channel presence-groupChat
. Typically, within the /send-message
route is where you will persist the message into a database, but for the purpose of this tutorial and to keep things simple, we won’t be persisting messages to database.
That’s all on the server side.
Setup chat app frontend
Let’s move on to building our app front-end. We talked about index.html
which will be the main entry point of our app and recall that we set up our server to render static files from within the public
directory. Now we’ll create the directory and the file.
mkdir public
cd public
touch index.html
And paste the following code into it:
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Group Chat App with Vue.js and Pusher</title>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
<style>
body {
padding-top: 100px;
}
.chat {
list-style: none;
margin: 0;
padding: 0;
}
.chat li {
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 1px dotted #B3A9A9;
}
.chat li .chat-body p {
margin: 0;
color: #777777;
}
.panel-body {
overflow-y: scroll;
height: 350px;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
background-color: #F5F5F5;
}
::-webkit-scrollbar {
width: 12px;
background-color: #F5F5F5;
}
::-webkit-scrollbar-thumb {
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
background-color: #555;
}
</style>
</head>
<body>
<div class="container" id="app">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="panel panel-info">
</div>
</div>
</div>
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://unpkg.com/vue"></script>
<script src="//js.pusher.com/4.0/pusher.min.js"></script>
<script src="./app.js"></script>
</body>
</html>
Basic Bootstrap webpage with some few stylings. As started in the introductory section of this tutorial, we’ll be using Vue.js. So we pull in Vue, Axios (will be used to make AJAX request to our server) and of course the Pusher JavaScript library. We also reference our own JavaScript file which will contain all our Vue.js specific code.
Now let’s add the main section of the chat app. Open index.html
and add the following code immediately after the div with class panel panel-info
:
<!-- public/index.html -->
<div class="panel-heading">
Group Chats <span class="badge">{{ members.count }}</span>
</div>
<div class="panel-body">
<div v-if="joined">
<em><span v-text="status"></span></em>
<ul class="chat">
<li class="left clearfix" v-for="message in messages">
<div class="chat-body clearfix">
<div class="header">
<strong class="primary-font">
{{ message.username }}
</strong>
</div>
<p>
{{ message.message }}
</p>
</div>
</li>
</ul>
<div class="panel-footer">
<div class="input-group">
<input id="btn-input" type="text" name="message" class="form-control input-sm" placeholder="Type your message here..." v-model="newMessage" @keyup.enter="sendMessage">
<span class="input-group-btn">
<button class="btn btn-primary btn-sm" id="btn-chat" @click="sendMessage">Send</button>
</span>
</div>
</div>
</div>
<div v-else>
<div class="form-group">
<input type="text" class="form-control" placeholder="enter your username to join chat" v-model="username" @keyup.enter="joinChat">
</div>
<button class="btn btn-primary" @click="joinChat">JOIN</button>
</div>
</div>
We have added Vue.js to the webpage, so we can start using it. We are displaying the number of users that have joined the group chat next to the Group Chat heading. Using Vue.js conditional rendering, we will only show the chat form when the user has joined (that is, entered his/her username) the group chat, else the user will be shown a form to enter a username to join chat. When the user clicks the JOIN button or presses ENTER while within the username input field, the joinChat
method will be triggered. We also bind the username input field with the username
data so we can easily pass whatever the user entered to Vue.js.
Once a user has joined the chat, we loop through (using v-for
) the messages (if there are any) and display them, then show the user an input field to type their chat message. When the user clicks the Send button or presses ENTER while within the message input field, the sendMessage
method will be triggered. We also bind the message input field with the newMessage
data so we can easily pass whatever the user entered to Vue.js.
When a user has not joined the chat:
When the user has joined the chat:
Create App.js
Now let’s create the file that will contain all our Vue.js specific code. Use the command below to do just that:
cd public
touch app.js
And paste the following code into it:
// public/app.js
const pusher = new Pusher('xxxxxxxxxxxxxxxxxxxx', {
cluster: 'APP_CLUSTER',
encrypted: true,
authEndpoint: 'pusher/auth'
});
const app = new Vue({
el: '#app',
data: {
joined: false,
username: '',
members: '',
newMessage: '',
messages: [],
status: ''
},
methods: {
joinChat() {
axios.post('join-chat', {username: this.username})
.then(response => {
// User has joined the chat
this.joined = true;
const channel = pusher.subscribe('presence-groupChat');
channel.bind('pusher:subscription_succeeded', (members) => {
this.members = channel.members;
});
// User joins chat
channel.bind('pusher:member_added', (member) => {
this.status = `${member.id} joined the chat`;
});
// Listen for chat messages
this.listen();
});
},
sendMessage() {
let message = {
username: this.username,
message: this.newMessage
}
// Clear input field
this.newMessage = '';
axios.post('/send-message', message);
},
listen() {
const channel = pusher.subscribe('presence-groupChat');
channel.bind('message_sent', (data) => {
this.messages.push({
username: data.username,
message: data.message
});
});
}
}
});
Remember to replace the xs with your Pusher app key and also specify your app cluster.
First, we create an instance of Pusher, passing to it our app key and other options. We pass in the auth endpoint /pusher/auth
we set up in our Express server. Next we create an instance of Vue.js. Then we specify the element (#app
) we want to mount Vue.js on. We define some data that will be used by our app. The joined
data which is false
by default indicates whether a user has joined the chat or not. The username
will be the username a user enters from the join chat form. The members
will hold the number of users in the group chat. The newMessage
will be the message a user enters from the chat form. The messages
will hold an array of chat messages. The status
will simply display notification of newly joined users.
When a user enters a username to join chat, the joinChat
method is called which in turn make an HTTP POST request to the /join-chat
endpoint on our server passing along the username. Once the request is successful, we set joined
to true
and subscribe the user to the presence-groupChat
channel. Because we’re using a presence channel, Pusher provides us with additional functionality like getting the members in the channel. With that, we can listen for when the subscription is successful and get all the users subscribed to the channel and assign it to our members
data. We also listen for when a new user joins the chat using the pusher:member_added
event and notify other users that a new user has joined the chat. Lastly, we call a listen
method (which we’ll create shortly) which listens for new chat messages and broadcast to users.
The sendMessage
method is pretty simple, it makes an HTTP POST request to the /send-message
endpoint on our server passing along the chat message. We also clear the input field of the previous message so the user can start typing new messages without having to manually clear the input field first.
Lastly, the listen
method which we called from within the joinChat
method will listen for message_sent
event that was triggered from our server and add the new message to the messages array and finally broadcast it to users.
Note: By default, Pusher will try to use /pusher/auth
as authEndpoint
. If you setup yours different from that, you’ll have to specify while instantiating the Pusher JavaScript library.
Note: Pusher presence channels are to be prefixed with presence-
.
Testing The App
So we’re done with building our group chat app. It’s time test out what we’ve built. To test the app, we need to start up our Express server by executing the command below:
node server.js
The app should be running now and can be access through http://localhost:3000. Go on and try the app out. You should get something similar as the demo below:
Conclusion
So that’s it for this tutorial. In this tutorial we saw how to build a group chat app with JavaScript and Pusher. Using Pusher’s presence channel, we saw how easy it is to get all the members subscribed to a channel.
24 April 2017
by Chimezie Enyinnaya