DEV Community

Parul Chandel
Parul Chandel

Posted on

How to make a Chat Client in JavaScript?

Requirements

  • Real-Time Client to Client Communication
  • Scalable - Allows multiple Users without any drop in performance
  • Every Client should see the same messages

Stack to be used

Node.js (Express.js + Socket.io), HTML, CSS

We can also use libraries like React or Vue or Angular to create the frontend.

Process -

  1. Create a server file that serves a simple HTML file using Express Static Serving.
// Importing Express Framework
const express = require('express');
// Creating an Express Instance
var app = express();

// Tell express to serve the desired static files on this instance
app.use(express.static(__dirname));

// Create a server event on port 3000
var server = app.listen(3000, ()=>{
    console.log("Server is running on 127.0.0.1:", server.address().port);
});
  1. Design your HTML File for the same.
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My Chat Room</title>
    <!-- Compiled and minified CSS -->
    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
    <script
  src="https://code.jquery.com/jquery-3.5.1.min.js"
  integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="
  crossorigin="anonymous"></script>
</head>
<body>
    <div class="container">
        <h1 class="center">
            Welcome to the Chat Room
        </h1>
        <div class="row">
            <div class="row">
                <div class="input-field col l12 m12 s12">
                  <input value="" id="name" type="text" class="validate">
                  <label class="active" for="first_name2">Name</label>
                </div>
            </div>
            <div class="row">
                <div class="input-field col l12 m12 s12">
                    <input value="" id="message" type="text" class="validate">
                    <label class="active" for="message">Message</label>
                  </div>
            </div>
            <div class="row">
                <a class="waves-effect waves-light btn" id='send'><i class="material-icons left">send</i>Send</a>
            </div>
        </div>
        <div class="row">
            <div id="messages">

            </div>
        </div>
    </div>
<script>

  $(document).ready(function() {
    M.updateTextFields();
    $('#send').click(()=>{
        addMessages({name:"Parul", text:"Hello World"});
    })
    function addMessages(message){
        $('#messages').append(`<div class="row"><b>${message.name}</b><p>${message.text}</p></div>`)
    }
  });
</script>
<!-- Compiled and minified JavaScript -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
</body>
</html>

Here, whenever the send button is clicked a default entry {name: "Parul", text: "Hello World"} is added to the messages div.

  1. Create an API to serve messages from the backend and consume the same data in the frontend by making a GET request.

server.js

// Importing Express Framework
const express = require('express');
// Creating an Express Instance
var app = express();

// Create a message array to emulate for frontend 
// (This has to be served by the database in next step)
var messages = [
    {name:"Parul", text:"How're you doing?"},
    {name:"Aman", text:"I'm fine. Thank You!"}
]

// Routing for /messages route - if we receive a GET request, send the messages 
//(API for message to use in frontend)
app.get('/messages', (req, res)=>{
    res.send(messages);
})

// Tell express to serve the desired static files on this instance
app.use(express.static(__dirname));

// Create a server event on port 3000
var server = app.listen(3000, ()=>{
    console.log("Server is running on 127.0.0.1:", server.address().port);
});

Here, we have created an API endpoint on /messages to serve the messages and we'll use this API endpoint to make a GET Request from the frontend

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My Chat Room</title>
    <!-- Compiled and minified CSS -->
    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
    <script
  src="https://code.jquery.com/jquery-3.5.1.min.js"
  integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="
  crossorigin="anonymous"></script>
</head>
<body>
    <div class="container">
        <h1 class="center">
            Welcome to the Chat Room
        </h1>
        <div class="row">
            <div class="row">
                <div class="input-field col l12 m12 s12">
                  <input value="" id="name" type="text" class="validate">
                  <label class="active" for="first_name2">Name</label>
                </div>
            </div>
            <div class="row">
                <div class="input-field col l12 m12 s12">
                    <input value="" id="message" type="text" class="validate">
                    <label class="active" for="message">Message</label>
                  </div>
            </div>
            <div class="row">
                <a class="waves-effect waves-light btn" id='send'><i class="material-icons left">send</i>Send</a>
            </div>
        </div>
        <div class="row">
            <div id="messages">

            </div>
        </div>
    </div>
<script>

  $(document).ready(function() {
        // Document load

        // Materialize requirements
    M.updateTextFields();

        // when send button is clicked, add the default json object to the messages div
    $('#send').click(()=>{
        addMessages({name:"Parul", text:"Hello World"});
    })
        // Load messages from backend api on document load
    getMessages()
  });
    function addMessages(message){
                // Add message on click
        $('#messages').append(`<div class="row"><b>${message.name}</b><p>${message.text}</p></div>`)
    }
    function getMessages()
    {
                // Load messages from backend api endpoint
        $.get('http://127.0.0.1:3000/messages', (data)=>{
                        // For each message object run addMessages function
            data.forEach(addMessages);
        })
    }
</script>
<!-- Compiled and minified JavaScript -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
</body>
</html>
  1. Handle POST Request - let the user send custom data to the messages.
  • Create a POST Request Route in the backend and test it using postman

    server.js

    // Importing Express Framework
    const express = require('express');
    // Creating an Express Instance
    var app = express();
    
    // Create a message array to emulate for frontend (This has to be served by the database in next step)
    var messages = [
        {name:"Parul", text:"How're you doing?"},
        {name:"Aman", text:"I'm fine. Thank You!"}
    ]
    
    // Routing for /messages route - if we receive a GET request, send the messages (API for message to use in frontend)
    app.get('/messages', (req, res)=>{
        res.send(messages);
    })
    // // Routing for /messages route - if we receive a POST request, get the data in the messages form (API for message to use in frontend)
    app.post('/messages', (req, res)=>{
        console.log(req.body);
        res.sendStatus(200);
    
    })
    // Tell express to serve the desired static files on this instance
    app.use(express.static(__dirname));
    
    // Create a server event on port 3000
    var server = app.listen(3000, ()=>{
        console.log("Server is running on 127.0.0.1:", server.address().port);
    });
    

Then let's test the endpoint response using Postman.

We send a POST Request to our messages endpoint with some JSON data.

POST Request in Postman

And we see that in the terminal, the req.body is logged as undefined.

This is because req.body is not parsed as JSON. We need body-parser module to do that.

body-parser is a middleware module that tells express to parse every request/response as JSON.

Hence, we need to install body-parser locally by doing - npm install -s body-parser in the terminal.

Then, we need to import body-parser into our server file and tell Express to use its JSON Parser as a middleware.

server.js

// Importing Express Framework
const express = require('express');
// Importing Body Parser Module
const bodyParser = require('body-parser');
// Creating an Express Instance
var app = express();

// Express Middleware Statements -> app.use()
// Tell express to serve the desired static files on this instance
app.use(express.static(__dirname));
app.use(bodyParser.json());

// Create a message array to emulate for frontend (This has to be served by the database in next step)
var messages = [
    {name:"Parul", text:"How're you doing?"},
    {name:"Aman", text:"I'm fine. Thank You!"}
]

// Routing for /messages route - if we receive a GET request, send the messages (API for message to use in frontend)
app.get('/messages', (req, res)=>{
    res.send(messages);
})
// // Routing for /messages route - if we receive a POST request, get the data in the messages form (API for message to use in frontend)
app.post('/messages', (req, res)=>{
    console.log(req.body);
    res.sendStatus(200);

})
// Create a server event on port 3000
var server = app.listen(3000, ()=>{
    console.log("Server is running on 127.0.0.1:", server.address().port);
});

Now if we run the same request using Postman, we'll see the JSON response in the terminal and 200 status code.

Now, to add the message to the API, we just need to push the message object into the messages array.

The app.post method should look something like this -

app.post('/messages', (req,res)=>{
    messages.push(req.body);
    res.sendStatus(200);
}

We now need to get the input from the form element in the HTML and then make a POST request to the /messages endpoint to let the user POST the message.

We are using jQuery to make the Requests from the Frontend. In the script tag where we added jQuery code last time, we'll need to make the following changes.

index.html

<script>

  $(document).ready(function() {
        // Document load

        // Materialize requirements
    M.updateTextFields();

        // when send button is clicked, add the default json object to the messages div
    $('#send').click(()=>{
                // Extract the input values using the respective id and pass it to the postMessage function
        postMessage({name:$('#name').val(), text:$('#message').val()});
    })
        // Load messages from backend api on document load
    getMessages()
  });
    function addMessages(message){
                // Add message on click
        $('#messages').append(`<div class="row"><b>${message.name}</b><p>${message.text}</p></div>`)
    }
    function postMessage(message)
    {
            // Function to Post New Message using the Form
      console.log(message);
      $.post('http://127.0.0.1:3000/messages', message);
    }

    function getMessages()
    {
                // Load messages from backend api endpoint
        $.get('http://127.0.0.1:3000/messages', (data)=>{
                        // For each message object run addMessages function
            data.forEach(addMessages);
        })
    }
</script>

However, if we try this out, we'll see a problem -

Alt Text

The values are shown as undefined.

This is happening because the response sent by the browser is url-encoded.

Hence, we need to tell body-parser to keep that in mind to decode the output.

To do that, we need to add this line to the server.js file.

app.use(bodyParser.urlencoded({extended:false}));

Now, if we add a new message and refresh, it will be displayed properly.

A few things are missing from the app right now -

  1. We are not able to add Messages without reloading the page.
  2. We are not storing the messages in a database. The messages array is hardcoded.

Setting up Socket.io to enable Polling/WebSockets

Sockets.io is a library that allows us to use WebSockets in our Node.js apps.

Polling - A process of pinging the server after a certain time to check for changes in the data.

e.g. Push Notifications on Facebook use WebSockets to ping the server

  1. Setting up socket.io is tricky -

    • Install socket.io using npm install -s socket.io
    • socket.io instance needs a node http server instance to run on. Hence we first need to create an HTTP Server using Node.js and then pass it to socket.io as an argument.

      Add the following to your server.js file after the creation of the Express App Instance.

      // Creating a HTTP Server Instance on our app for Socket.io to use
      var http = require('http').Server(app);
      // Passing the server instance to the io instance
      var io = require('socket.io')(http);
      
- We then need to include the `socket.io.js` file into our HTML.

    Add the following line to index.html where you import all the JS Files.
    ```jsx
    <script src='/socket.io/socket.io.js'></script>
    ```
    When you start the app, you'll notice another problem. The `[socket.io](http://socket.io).js` file is not found by express. This is happening because our socket.io instance is bound to the HTTP Server, not the Express App Server. Hence, now we need to use the HTTP Server for our App to run on.

    To do this, we just need to change the `app.listen(`) at the end of the server.js file to `http.listen()`

    Now, everything should run fine.
  1. Now we need to connect the socket.io when our webpage is loaded. To achieve this, we need to declare a socket instance in the script tag of our HTML file.

    <script>
        // Declaring a socket instance
      var socket = io();
    
      $(document).ready(function() {
            // Document load
    
            // Materialize requirements
        M.updateTextFields();
    
            // when send button is clicked, add the default json object to the messages div
        $('#send').click(()=>{
            postMessage({name:$('#first_name2').val(), text:$('#message').val()});
        })
            // Load messages from backend api on document load
        getMessages()
      });
        function addMessages(message){
                    // Add message on click
            $('#messages').append(`<div class="row"><b>${message.name}</b><p>${message.text}</p></div>`)
        }
    
        function postMessage(message)
        {
          console.log(message);
          $.post('http://127.0.0.1:3000/messages', message);
        }
    
        function getMessages()
        {
                    // Load messages from backend api endpoint
            $.get('http://127.0.0.1:3000/messages', (data)=>{
                            // For each message object run addMessages function
                data.forEach(addMessages);
            })
        }
    </script>
    

    Then, we can check for the number of connections to the socket using a simple addition in the server.js file.

    // Using the event hook on our socket instance in server.js
    io.on('connection', (socket)=>{
        console.log('A user was just connected');
    });
    
  2. Next → to start a realtime connection between clients and notify all clients when a new message arrives, we need to emit an event whenever a new message arrives in the backend and then, listen for that event from the frontend and add the message to the list.

    server.js

    app.post('/messages', (req,res)=>{
        // Code //
        // In the end, if message is send succesfully, emit the new_message event
        // to inform all the clients of the new message
        io.emit('new_message', req.body);
    }
    

    In the Frontend, we need an event listener to listen for the new_messages event and get the new view accordingly.

    index.html

    <script>
    var socket = io();
    
    // Code // 
    
    socket.on('new_message', addMessages);
    // code //
    </script>
    
  3. Still, our messages are stored as hardcode. We need to remove that using a database connection. We'll be using MongoDB here.

  • Create a collection on MLab and get the user login URL
  • To interact with MongoDB, Node.js uses a package called mongoose. So, we need to install it using npm install -s mongoose
  • We then use require('mongoose') into our application and try to establish a connection using the .connect() method of mongoose.
// The login URL is stored in the dburl variable
mongoose.connect(dburl, {useMongoClient:true, useNewUrlParser:true, useUnifiedTopology:true}, (err)=>{
    console.log("MongoDB Connection Established!!!\n", err);
});
  • Then we'll need to create a model for the message to be saved as using .model() function on Mongoose.
// The first argument is an alias name for the model to search on
// Second argument is the schema itself
var MessageModel = mongoose.model('MessageModel', {
    name:String,
    text:String,
});
  • After that, we need to model our message using this model when we send them. So, in our app.post() method, we send the req.body to the model to convert that to the given structure, and then save it to the DB using .save() function of Mongoose.
app.post('/messages', (req,res)=>{
    // Model the request body as per the message model structure
    var message = MessageModel(req.body);
    // Save to db and return error if occuring
    message.save((err)=>{
        if(err)
            res.sendStatus(500);
        res.sendStatus(200);
        // Emit the new_message event only if the message is successfully saved
        io.emit('new_message', req.body);
    }
});
  • Now, we need to set up the app.get() method as well to show all the messages saved in the Database. So, we will retrieve all the messages from the database and send them in the response.
app.get('/messages', (req,res)=>{
    // Find all the messages and return as response
    // model.find has two arguments - criteria and callback
    MessageModel.find({}, (err, messages)=>{
        res.send(messages);
    });
});

Our App is now Complete!!! We can do minor upgrades and Changes in Code now!!

Testing using Jasmine

Jasmine is a Testing Framework for JS Apps. There a lot of other frameworks as well(e.g. Mocha) but Jasmine offers good learning curve for beginners.

Steps for Setting up Jasmine -

  • To install Jasmine, just execute npm install —save-dev jasmine. The —save-dev flag tells npm that its a development phase dependency and is not actually needed for the actual production build.

  • Then, we need to create something called specs folder in Jasmine. Specs are test files in Jasmine. To do that, execute - ./node_modules/.bin/jasmine init in the terminal. A new folder named spec will be created in the App Folder.

  • Now, in your package.json file, under the scripts field, change the value of test to jasmine. This will test npm to execute jasmine whenever we call npm test from the terminal.

  • Then we need to create test files in the spec folder. A test file always follows this format - <name>.spec.js in Jasmine

  • We will create a file name server.spec.js .We need to test the following things -

    • Whether the list of messages is returned successfully.
    • Whether the list of messages is empty or not

    To make requests from within the spec file, we'll need the request module of node.js, and then every time we test the app, we first need to serve the app for the test to execute successfully.

    server.spec.js

    // Request Module needed to make requests from the test file
    var request = require('request');
    
    // Describe a new test instance on getting the messages
    describe('get messages', ()=>{
            // it keyword is used to define the properties (arguments - alis, callback)
        it('should return 200 ok', (done)=>{
                    // Done Keyword is used to create a async call for the req to execute first
            request.get('http://127.0.0.1:3000/messages', (err, res)=>{
                            // expect takes the output variable and compares it to a given value
                expect(res.statusCode).toEqual(200);
                            // Test is called when the request is done (Async Test)
                done()
            })
        })
            // Test for non empty message list
        it('should return a non-empty list', (done)=>{
            request.get('http://127.0.0.1:3000/messages', (err, res)=>{
                            // Expect an array whose length >= 2
                expect(JSON.parse(res.body).length).toBeGreaterThanOrEqual(2);
                done()
            })
        })
    })
    

    We can now execute the tests by starting the server first and then using npm test

And That's it. Our Chat Client is now ready!

Top comments (3)

Collapse
 
geekyahmed profile image
Ahmed Bankole

Thanks for the tutorial

Collapse
 
parulc7 profile image
Parul Chandel

You're Welcome

Collapse
 
geekyahmed profile image
Ahmed Bankole

😊😊