Creating a Simple Real-Time Chat Application with Socket.IO
Tags: webdev, javascript, beginners, tutorial
Published: 2024-07-13 17:36:46

Let's build a simple chat app using Socket.IO with chat rooms. Here's the live site link [Simple Chat](https://simple-chat-ycdq.onrender.com/). 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**
```json
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
```

**Initialize the Project**

```js
npm init -y
```

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

```js
npm install express nodemon
```

let's configure the nodemon in package.json
```json
 "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js",
    "dev":"nodemon server.js"
  },
```

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

```js
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}`);
});
```

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.

```js
npm run dev      //start the server
```

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

```js
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}`);
});
```

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

let's integrate socket io

```js
npm install socket.io
```

```js
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');
});
```

Now in the index.html

```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>
```
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

```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>
```
and print out the message received from the client.

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

**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.

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

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.

```html
<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>
```

the output image is shown below.

![output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vgl0xd2pm4lntvg6r9dg.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2819r1o1t8atjs59ggpt.png)

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`

```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>
```

In the `createRoom.html`

```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>
```

In the `JoinRoom.html`

```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>
```

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.

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

in `server.js`

```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");
  }
});
```

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`

```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");
  }
});
```

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`

```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();
  }
});
```

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`

```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 });
    }
  });
});
```

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`

```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}`);
});
```

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
```js
const io = new Server(server, {
  connectionStateRecovery: {},
});
```

and in the client script.js

```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();
  }
});
```

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](https://github.com/sanjayr-12/simple-chat)
here's the live site link - [Simple Chat](https://simple-chat-ycdq.onrender.com/)

Thank You!!!

Follow me on: [Linkedin](https://www.linkedin.com/in/sanjay-r-ab6064294/),  [Medium](https://medium.com/@sanjayxr), [Instagram](https://www.instagram.com/_sanjayxr_12_/), [X](https://twitter.com/sanjayxr_12)