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 -
- 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);
});
- 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
.
- 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>
- 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.
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 -
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 -
- We are not able to add Messages without reloading the page.
- 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
-
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);
- Install socket.io using
- 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.
-
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'); });
-
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>
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 thereq.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 namedspec
will be created in the App Folder.Now, in your
package.json
file, under thescripts
field, change the value oftest
tojasmine
. This will test npm to executejasmine
whenever we callnpm 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)
Thanks for the tutorial
You're Welcome
šš