DEV Community

Cover image for Building a Simple Real-Time Chat Application with Socket.IO
Sanjay R
Sanjay R

Posted on • Edited on

Building a Simple Real-Time Chat Application with Socket.IO

Let's build a simple chat app using Socket.IO with chat rooms. Here's the live site link Simple Chat. Don't judge the UI πŸ˜…. I know it is not good and it is not even responsive. For now, let's focus on the concepts and functionality.

Before diving into the code, let's cover some concepts about WebSockets and Socket.IO.

WebSockets
WebSockets is a protocol that provides full-duplex communication, meaning it allows a connection between the client and the server, enabling real-time, two-way communication.

Socket IO
Socket.IO is a JavaScript library that simplifies the use of WebSockets. It provides an abstraction over WebSockets, making real-time communication easier to implement.

Let's dive into the coding part. Make sure Node.js is installed on your computer.

Folder Structure



server/
β”œβ”€β”€ node_modules/
β”œβ”€β”€ public/
β”‚   β”œβ”€β”€ chat.html
β”‚   β”œβ”€β”€ createRoom.html
β”‚   β”œβ”€β”€ index.html
β”‚   β”œβ”€β”€ JoinRoom.html
β”‚   β”œβ”€β”€ script.js
β”‚   └── styles.css
β”œβ”€β”€ .env
β”œβ”€β”€ .gitignore
β”œβ”€β”€ package-lock.json
β”œβ”€β”€ package.json
└── server.js


Enter fullscreen mode Exit fullscreen mode

Initialize the Project



npm init -y


Enter fullscreen mode Exit fullscreen mode

install the required packages, first, let's install the express and nodemon



npm install express nodemon


Enter fullscreen mode Exit fullscreen mode

let's configure the nodemon in package.json



 "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js",
    "dev":"nodemon server.js"
  },


Enter fullscreen mode Exit fullscreen mode

let's set up our application, and create a file name server.js or index.js as you wish.



const express = require("express");
const env = require("dotenv");
const app = express();
const port = process.env.PORT || 3000;
const { createServer } = require("node:http");  
const server = createServer(app);

//when the user enters the localhost:3000, the request is handled by this route handler
app.get('/', (req, res) => {           //get request
  res.send('<h1>Hello world</h1>');    
});

server.listen(port, () => {               //listen on the port
  console.log(`server started ${port}`);
});


Enter fullscreen mode Exit fullscreen mode

socket.io requires direct access to the underlying HTTP server to establish WebSocket connections. so we require the createServer from the HTTP module. and listen on the port.



npm run dev      //start the server


Enter fullscreen mode Exit fullscreen mode

we are not going to use any frontend frameworks, so let's serve the HTML static file from the server.Β 
create a folder public and create an index.html inside the public folder



const path = require("path");    //require the path module

app.use(express.static("public"));   //make the folder as static 

//in the app.get let's send the html
app.get("/", (req, res) => {
  res.sendFile(path.join(__dirname, "public", "index.html"));
});

server.listen(port, () => {               //listen on the port
  console.log(`server started ${port}`);
});


Enter fullscreen mode Exit fullscreen mode

Now if you open the localhost, you can see the HTML page.

let's integrate socket io



npm install socket.io


Enter fullscreen mode Exit fullscreen mode


const { Server } = require('socket.io');  //require 
const io = new Server(server);  //initialize  the new instance of the socket.io by passing the server object

//listen on the connection event for incoming sockets
io.on('connection', (socket) => {    
  console.log('user connected');
});


Enter fullscreen mode Exit fullscreen mode

Now in the index.html



<body>
    <ul id="messages"></ul>
    <form id="form" action="">
      <input id="input" autocomplete="off" /><button>Send</button>
    </form>

<script src="/socket.io/socket.io.js"></script>
<script>
  const socket = io();   //establish a connection
</script>
</body>


Enter fullscreen mode Exit fullscreen mode

the above script is used to enable real-time communication between the client and the server.
when you open localhost you can see the user connected as the output.

Emit some events

The main idea of socket.io is that you can send and receive any data you want. JSON and Binary data are also supported.

Let's make it so that when the user sends the message, the server gets it as a chat message event.Β 

in the index.html



<script src="/socket.io/socket.io.js"></script>
<script>
  const socket = io();

  const form = document.getElementById('form');
  const input = document.getElementById('input');

  form.addEventListener('submit', (e) => {
    e.preventDefault();
    if (input.value) {
      socket.emit('message', input.value);
      input.value = '';
    }
  });
</script>


Enter fullscreen mode Exit fullscreen mode

and print out the message received from the client.



io.on('connection', (socket) => {
  socket.on('message', (msg) => {
    console.log('message: ' + msg);
  });
});


Enter fullscreen mode Exit fullscreen mode

Broadcasting
Now we have received the message from the client, then we need to broadcast it to all the users.

socket.io gives the io.emit() method, to emit the events or messages.



io.on('connection', (socket) => {
  socket.on('message', (msg) => {
    io.emit('message', msg);
  });
});


Enter fullscreen mode Exit fullscreen mode

So, now the messages from the client are broadcasted to all the users, we need to capture the message and include it in the page.



<script src="/socket.io/socket.io.js"></script>
<script>
  const socket = io();

  const form = document.getElementById('form');
  const input = document.getElementById('input');
  const messages = document.getElementById('messages');

  form.addEventListener('submit', (e) => {
    e.preventDefault();
    if (input.value) {
      socket.emit('message', input.value);
      input.value = '';
    }
  });

  socket.on('message', (msg) => {
    const item = document.createElement('li');
    item.textContent = msg;
    messages.appendChild(item);
  });
</script>


Enter fullscreen mode Exit fullscreen mode

the output image is shown below.

output

We have done the basic chat application, let's make chat rooms.

How do chat rooms work?

socket io provides functionality for the rooms, so the event is emitted, and only the room members can see the message.

chat room concept

Now I am going to create a 3 html file in the public folder. chat.htmlΒ , createRoom.htmlΒ , joinRoom.html

I am going to shift the code from the index.html to the chat.html.

In the index.html



<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Index</title>
  </head>
  <body>
    <div class="main">
      <div class="text">
        <h1>Chat</h1>
      </div>
      <button id="create-room">Create Room</button>
      <button id="join-room">Join Room</button>
    </div>
    <script>
      const createRoom = document.getElementById("create-room");
      const joinRoom = document.getElementById("join-room");

      createRoom.addEventListener("click", () => {
        location.href = "./createRoom.html";
      });

      joinRoom.addEventListener("click", () => {
        location.href = "./JoinRoom.html";
      });
    </script>
  </body>
</html>


Enter fullscreen mode Exit fullscreen mode

In the createRoom.html



<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Create Room</title>
</head>
<body>
    <div class="form-container">
        <h1>Create Room</h1>
        <form action="/create" method="post">
            <input type="text" placeholder="Enter the room name" name="roomName" required>
            <input type="submit" value="Create Room">
        </form>
    </div>
</body>
</html>


Enter fullscreen mode Exit fullscreen mode

In the JoinRoom.html



<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Join Room</title>
</head>
<body>
    <div class="form-container">
        <h1>Join Room</h1>
        <form action="/joinRoom" method="post">
            <input type="text" placeholder="Enter the room name" name="Join" required>
            <input type="submit" value="Join Room">
        </form>
    </div>
</body>
</html>


Enter fullscreen mode Exit fullscreen mode

So let me explain what I have done in this file, in the index.html I have made a UI with two buttons to create a room or join a room, it will be redirected to the respective files when it is clicked.

Creating the room
In createRoom.html, there's a form with the POST method, and the action is set to /create. When submitted, the request is sent to the server.



<form action="/create" method="post">
  <input type="text" placeholder="Enter the room name" name="roomName" required>
  <input type="submit" value="Create Room">
</form>


Enter fullscreen mode Exit fullscreen mode

in server.js



app.use(express.urlencoded({ extended: true }));  //parse the data

const rooms = [];    //to store the rooms

app.post("/create", (req, res) => {
  const roomName = req.body.roomName;   //get the room name from the body
  if (rooms.includes(roomName)) {       //see if the room already exists
    res.send("room already exits");
  } else if (roomName) {
    rooms.push(roomName);               //push the  room name to the array
    res.redirect(`/chat.html?room=${roomName}`);   //redirect to the chat.html with the room name
  } else {
    res.redirect("/createRoom.html");
  }
});


Enter fullscreen mode Exit fullscreen mode

we are not using any databases to store the room names, for this tutorial let's use an array to store the room names.

let's do the similar functionalities for the join room

in server.js



app.post("/joinRoom", (req, res) => {
  const roomName = req.body.Join;
  if (rooms.includes(roomName)) {
    res.redirect(`/chat.html?room=${roomName}`);
  } else {
    res.send("enter the valid room");
  }
});


Enter fullscreen mode Exit fullscreen mode

Let's make a separate JavaScript file script.js in the public. I have copied all the JS from the chat.html to a separate file.

script.js



const socket = io();

function getUrlname(name) {
  const urlPara = new URLSearchParams(window.location.search);
  return urlPara.get(name);
}

const roomName = getUrlname("room");
if (roomName) {
  socket.emit("join room", { roomName });
} else {
  console.log("room is not valid");
}

const form = document.getElementById("form");
const input = document.getElementById("input");
const messages = document.getElementById("messages");

form.addEventListener("submit", (e) => {
  e.preventDefault();
  if (input.value) {
    socket.emit("message", { roomName, message: input.value });
    input.value = "";
  }
});

socket.on("message", ({ message }) => {
  if (!message) {
    alert("room is not valid");
  } else {
    const item = document.createElement("li");
    console.log(message);
    item.textContent = message;
    messages.appendChild(item);
  }
});

// Connection state recovery
const disconnectBtn = document.getElementById("disconnect-btn");

disconnectBtn.addEventListener("click", (e) => {
  e.preventDefault();
  if (socket.connected) {
    disconnectBtn.innerText = "Connect";
    socket.disconnect();
  } else {
    disconnectBtn.innerText = "Disconnect";
    socket.connect();
  }
});


Enter fullscreen mode Exit fullscreen mode

In the server, after the create room or join room is handled it is redirected to the chat.html page, we have also sent the room name in the search params.Β 

In the above code, we are extracting the room name from the search params using URLSearchParams and checking if the room is valid.

Each message from the client is emitted with the message and with the room name. With the help of this we can now check from which room the message is from.

in server.js



io.on("connection", (socket) => {
  socket.on("join room", ({ roomName }) => {
    if (rooms.includes(roomName)) {
      socket.join(roomName);
    }
  });

  socket.on("message", ({ roomName, message }) => {
    if (rooms.includes(roomName)) {
      io.to(roomName).emit("message", { message });
    } else {
      socket.emit("message", { message: false });
    }
  });
});


Enter fullscreen mode Exit fullscreen mode

socket provides a join method to join the room, and we check where the message is from, and the message is broadcasted to only that room member. this is done by the io.to(roomName).emit("message",{message})

Now open multiple tabs on the browser create different rooms join in that room and check if this works.

here's the full code for server.js



const express = require("express");
const app = express();
const env = require("dotenv");
const path = require("path");
const { Server } = require("socket.io");
const { createServer } = require("node:http");
const server = createServer(app);
env.config();
const port = process.env.PORT;
app.use(express.static("public"));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

const io = new Server(server, {
  connectionStateRecovery: {},
});

const rooms = [];

app.get("/", (req, res) => {
  res.sendFile(path.join(__dirname, "public", "index.html"));
});

app.post("/create", (req, res) => {
  const roomName = req.body.roomName;
  if (rooms.includes(roomName)) {
    res.send("room already exits");
  } else if (roomName) {
    rooms.push(roomName);
    res.redirect(`/chat.html?room=${roomName}`);
  } else {
    res.redirect("/createRoom.html");
  }
});

app.post("/joinRoom", (req, res) => {
  const roomName = req.body.Join;
  if (rooms.includes(roomName)) {
    res.redirect(`/chat.html?room=${roomName}`);
  } else {
    res.send("enter the valid room");
  }
});

io.on("connection", (socket) => {
  socket.on("join room", ({ roomName }) => {
    if (rooms.includes(roomName)) {
      socket.join(roomName);
    }
  });

  socket.on("message", ({ roomName, message }) => {
    if (rooms.includes(roomName)) {
      io.to(roomName).emit("message", { message })
    } else {
      socket.emit("message", { message: false });
    }
  });
});

server.listen(port, () => {
  console.log(`server started ${port}`);
});


Enter fullscreen mode Exit fullscreen mode

we have done it!! and finally, let me tell one concept that is called connection state recovery.

In the server code, there is one line



const io = new Server(server, {
  connectionStateRecovery: {},
});


Enter fullscreen mode Exit fullscreen mode

and in the client script.js



// Connection state recovery
const disconnectBtn = document.getElementById("disconnect-btn");

disconnectBtn.addEventListener("click", (e) => {
  e.preventDefault();
  if (socket.connected) {
    disconnectBtn.innerText = "Connect";
    socket.disconnect();
  } else {
    disconnectBtn.innerText = "Disconnect";
    socket.connect();
  }
});


Enter fullscreen mode Exit fullscreen mode

it is used to handle the disconnections by pretending that there was no disconnection.
this feature will temporarily store all the events that are sent by the server and will try to restore the state when the client reconnects.

This is it!!!

here's the link for the source code - GitHub
here's the live site link - Simple Chat

Thank You!!!

Follow me on: Linkedin, Medium, Instagram, X

Top comments (0)