Build a Chat Room With Socket.io and Express

Written by zt4ff | Published 2022/11/06
Tech Story Tags: javascript | beginners | tutorial | chat | web-app-development | socket.io | expressjs | nodejs | web-monetization

TLDRIn this article, we are going to create a chat application that connects people, anonymously, to different rooms together in pairs of two rooms. The chat application would make use of **[Express.js] for the server-side code, and the client-side will be developed with vanilla JavaScript. We will create a JavaScript file, and name, and create a simple Node HTTP server. We would be creating a simple UI using Vanilla JavaScript and we serve the web page as a static file from our express app.via the TL;DR App

In this article, we are going to create a chat application that connects people, anonymously, to different rooms together in pairs of two. The chat application would make use of Express.js for the server-side code, listen to web socket communication using Socket.io and the client-side will be developed with vanilla JavaScript.

Setting up our project

  • We will create a directory named chat-app and change the directory to the directory using the command.
$ mkdir chat-app && cd chat-app
  • Initialize our Node application by running the command.
$ yarn init -y
  • Install express in our project using Yarn by running the command.
$ yarn add express
  • We will create a JavaScript file, and name, and create a simple Node HTTP server.
  • Next, we will import express into our application, create an express app and start the server to listen to requests on port 8001.
// app.js
const http = require("http")
const express = require("express")

const app = express()

app.get("/index", (req, res) => {
    res.send("Welcome home")
})

const server = http.createServer(app)

server.on("error", (err) => {
    console.log("Error opening server")
})

server.listen(8001, () => {
    console.log("Server working on port 8001")
})
  • Now we can start the application by running the command.
$ node app.js

You can visit [http://localhost:8001/index](http://localhost:8001/index) on your browser to test that the application works

Initializing socket.io on the server-side

To initialize a socket on the server side, follow the following steps.

  • Install socket.io dependency into our application by running the command.

    $ yarn add socket.io
    
  • Import socket.io into our code, create a new socket server, and then add an event listener to the socket to listen if a connection is made.

    // app.js
    const http = require("http");
    const { Server } = require("socket.io");
    const express = require("express");
    
    const app = express();
    
    app.get("/index", (req, res) => {
      res.send("Welcome home");
    });
    
    const server = http.createServer(app);
    
    const io = new Server(server);
    
    io.on("connection", (socket) => {
      console.log("connected");
    });
    
    server.on("error", (err) => {
      console.log("Error opening server");
    });
    
    server.listen(8001, () => {
      console.log("Server working on port 3000");
    });
    

Initializing socket.io on the client-side

We would be creating a simple UI using Vanilla JavaScript and we serve the web page as a static file from our express application.

We’d create a public directory including files to build up our UI, making our project structure look like this.

chat-app/
			|- node_modules/
			|- public/
						|- index.html
						|- main.js
			|- app.js
			|- package.json
			|- yarn.lock

We are going to be making use of Tailwind CSS to style the Client UI to reduce the amount of custom CSS we’d be writing.

In the index.html, create a template for our chat window.

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://cdn.tailwindcss.com"></script>
    <title>Anon Chat App</title>
</head>
<body>
    <div class="flex-1 p:2 sm:p-6 justify-between flex flex-col h-screen">
        <div id="messages" class="flex flex-col space-y-4 p-3 overflow-y-auto scrollbar-thumb-blue scrollbar-thumb-rounded scrollbar-track-blue-lighter scrollbar-w-2 scrolling-touch">
        </div>
        <div class="border-t-2 border-gray-200 px-4 pt-4 mb-2 sm:mb-0">
           <div class="relative flex">
              <input type="text" placeholder="Write your message!" class="w-full focus:outline-none focus:placeholder-gray-400 text-gray-600 placeholder-gray-600 pl-12 bg-gray-200 rounded-md py-3">
              <div class="absolute right-0 items-center inset-y-0 hidden sm:flex">
                 <button type="button" class="inline-flex items-center justify-center rounded-lg px-4 py-3 transition duration-500 ease-in-out text-white bg-blue-500 hover:bg-blue-400 focus:outline-none">
                    <span class="font-bold">Send</span>
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="h-6 w-6 ml-2 transform rotate-90">
                       <path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z"></path>
                    </svg>
                 </button>
              </div>
           </div>
        </div>
     </div>
		 <script src="/socket.io/socket.io.js"></script>
     <script src="./main.js"></script>
</body>
</html>

In the HTML file above we included two JavaScript files, the first one to initialize socket.io on the client side and another main.js file to write our custom JavaScript code.

Then in the main.js file, we would create a function that can add a message to the chatbox. The function createMessage will expect two arguments. The first argument is the message string and the second argument is a boolean to determine if the message is from the user or

another external user.

// main.js
const messageBox = document.querySelector("#messages");

function createMessage(text, ownMessage = false) {
  const messageElement = document.createElement("div");
  messageElement.className = "chat-message";
  const subMesssageElement = document.createElement("div");
  subMesssageElement.className =
    "px-4 py-4 rounded-lg inline-block rounded-bl-none bg-gray-300 text-gray-600";
  if (ownMessage) {
    subMesssageElement.className += " float-right bg-blue-800 text-white";
  }
  subMesssageElement.innerText = text;
  messageElement.appendChild(subMesssageElement);

  messageBox.appendChild(messageElement);
}

createMessage("Welcome to vahalla");
createMessage("Who are you to talk to me", true);

Change the code in the server application, app.js and make use of static files to render the client UI.

// app.js
const http = require("http");
const { Server } = require("socket.io");
const express = require("express");
const path = require("path");

const app = express();

app.use(express.static(path.join(__dirname, "public")));

const server = http.createServer(app);

const io = new Server(server);

io.on("connection", (socket) => {
  console.log("connected");
});

server.on("error", (err) => {
  console.log("Error opening server");
});

server.listen(8001, () => {
  console.log("Server working on port 8001");
});

NOTE: To view the changes made in our application, we have to stop the running server application and re-run it for the new changes to take effect. So we are making use of nodemon to automate this process for us.

Install nodemon by running.

$ npm install -g nodemon

Then run the node application using nodemon.

$ nodemon ./app.js

Open [http://localhost:8001](http://localhost:3000) on your browser to view what the chat app would look like.

Creating different rooms for Web Socket communication

To keep track of the rooms created and the number of users connected to each room, we will create a Room class to manage this data for us.

We’d create a new file named room.js in the root directory of our project. Then we create the Room class and have the constructor initialize a property for keeping the state of our room.

// room.js

// the maximum number of people allowed in each room
const ROOM_MAX_CAPACITY = 2;

class Room {
  constructor() {
    this.roomsState = [];
  }
}

module.exports = Room;

The roomsState is an array of objects that keeps the information about each room ID created and the number of users in that room. So a typical roomsState would look like this.

// rooms state
[
	{
		roomID: "some id",
		users: 1
	},
	{
		roomID: "a different id",
		users: 2
	}
]

Next, add a method to join a room. The method will loop through the room to check if any rooms have several users that are less than the maximum number of participants allowed in each room. If all room in the list is occupied, it would create a new room and initialize the number of users in that room to 1.

To generate a unique id, we would be making use of a package known as UUID in our application.

Install uuid by running this command in our terminal.

$ yarn add uuid

Then import the package into our application by running as follows.

// room.js
const { v4: uuidv4 } = require("uuid");

class Room {
  constructor() {
    /**/
  }

  joinRoom() {
    return new Promise((resolve) => {
      for (let i = 0; i < this.roomsState.length; i++) {
        if (this.roomsState[i].users < ROOM_MAX_CAPACITY) {
          this.roomsState[i].users++;
          return resolve(this.roomsState[i].id);
        }
      }

      // else generate a new room id
      const newID = uuidv4();
      this.roomsState.push({
        id: newID,
        users: 1,
      });
      return resolve(newID);
    });
  }
}

module.exports = Room;

NOTE: Making use of an array to manage the rooms' state is, obviously, not the best way to do so. Imagine having thousands of rooms in your application and you have to loop through each room for each join request. It would execute at O(n). For the purpose of this tutorial, we will stick to this approach.

We’d add another method to the Room class, leaveRoom(), to reduce the number of users in a particular room.

// room.js
class Room {
  constructor() {
    /**/
  }

  joinRoom() {}

  leaveRoom(id) {
    this.roomsState = this.roomsState.filter((room) => {
      if (room.id === id) {
        if (room.users === 1) {
          return false;
        } else {
          room.users--;
        }
      }
      return true;
    });
  }
}

module.exports = Room;

The leaveRoom() the method takes a room ID, and loops through the array of rooms to find if any of the rooms match the ID provided in the argument.

If it finds the matching room, it checks if the user in the room is one to delete that particular room state. If the user in the room is greater than 1, the leaveRoom() the method just deducts the number of users in that room by one.

Finally, our room.js code should be similar to this.

// room.js
const { v4: uuidv4 } = require("uuid");

// the maximum number of people allowed in a room
const ROOM_MAX_CAPACITY = 2;

class Room {
  constructor() {
    this.roomsState = [];
  }

  joinRoom() {
    return new Promise((resolve) => {
      for (let i = 0; i < this.roomsState.length; i++) {
        if (this.roomsState[i].users < ROOM_MAX_CAPACITY) {
          this.roomsState[i].users++;
          return resolve(this.roomsState[i].id);
        }
      }

      const newID = uuidv4();
      this.roomsState.push({
        id: newID,
        users: 1,
      });
      return resolve(newID);
    });
  }

  leaveRoom(id) {
    this.roomsState = this.roomsState.filter((room) => {
      if (room.id === id) {
        if (room.users === 1) {
          return false;
        } else {
          room.users--;
        }
      }
      return true;
    });
  }
}

module.exports = Room;

Joining and leaving the rooms.

To create different channels for users in our chat application, we would be creating rooms for them.

socket.io allows us to create arbitrary channels that sockets can join and leave. It can be used to broadcast events to a subset of clients.

(source: https://socket.io/docs/v3/rooms/)

To join a room, we would join a room with a unique room ID.

io.on("connection", socket => {
	// join a room
  socket.join("some room id");

  socket.to("some room id").emit("some event");
});

In our server application, once new users join the connection, the Room.joinRoom() returns a unique ID which is our unique room ID. So we can join and leave room in our rooms as follow.

// app.js
io.on("connection", async (socket) => {
  const roomID = await room.joinRoom();
  // join room
  socket.join(roomID);

  socket.on("disconnect", () => {
    // leave room
    room.leaveRoom(roomID);
  });
});

Sending and receiving messages

Now, we’d move back to our client-side code to emit events for messages sent from the client. And also listen to message events coming from the server and write that messages to our chatbox.

// main.js
socket.on("receive-message", (message) => {
  createMessage(message);
});

sendButton.addEventListener("click", () => {
  if (textBox.value != "") {
    socket.emit("send-message", textBox.value);
    createMessage(textBox.value, true);
    textBox.value = "";
  }
});

NOTE: In our chat application, we directly add the message from user to the chatbox without confirming if the message is received by the socket server. This is not usually the case.

Then on our express application.

// app.js
io.on("connection", async (socket) => {
  const roomID = await room.joinRoom();
  // join room
  socket.join(roomID);

  socket.on("send-message", (message) => {
    socket.to(roomID).emit("receive-message", message);
  });

  socket.on("disconnect", () => {
    // leave room
    room.leaveRoom(roomID);
  });
});

Making our express application code look like this finally.

// app.js
const http = require("http");
const { Server } = require("socket.io");
const express = require("express");
const path = require("path");
const Room = require("./room");

const app = express();

app.use(express.static(path.join(__dirname, "public")));

const server = http.createServer(app);

const io = new Server(server);

const room = new Room();

io.on("connection", async (socket) => {
  const roomID = await room.joinRoom();
  // join room
  socket.join(roomID);

  socket.on("send-message", (message) => {
    socket.to(roomID).emit("receive-message", message);
  });

  socket.on("disconnect", () => {
    // leave room
    room.leaveRoom(roomID);
  });
});

server.on("error", (err) => {
  console.log("Error opening server");
});

server.listen(8001, () => {
  console.log("Server working on port 8001");
});

And our client-side JavaScript looks like this.

// main.js
const messageBox = document.querySelector("#messages");
const textBox = document.querySelector("input");
const sendButton = document.querySelector("button");

function createMessage(text, ownMessage = false) {
  const messageElement = document.createElement("div");
  messageElement.className = "chat-message";
  const subMesssageElement = document.createElement("div");
  subMesssageElement.className =
    "px-4 py-4 rounded-lg inline-block rounded-bl-none bg-gray-300 text-gray-600";
  if (ownMessage) {
    subMesssageElement.className += " float-right bg-blue-800 text-white";
  }
  subMesssageElement.innerText = text;
  messageElement.appendChild(subMesssageElement);

  messageBox.appendChild(messageElement);
}

const socket = io();

socket.on("connection", (socket) => {
  console.log(socket.id);
});

socket.on("receive-message", (message) => {
  createMessage(message);
});

sendButton.addEventListener("click", () => {
  if (textBox.value != "") {
    socket.emit("send-message", textBox.value);
    createMessage(textBox.value, true);
    textBox.value = "";
  }
});

Testing our chat app

To text our chat app, we will open four different browsers to confirm that two rooms are created.

Conclusion

If you see this, it means that we read thus far and probably have the chat app running on our machine.

You can find the code from this article in this GitHub repository.

To include more challenges, these are features you can include in the chat application

  • Inform users if people left or joined the room
  • Refactor the rooms state array to a more efficient data structure
  • Allow pairing based on topic selection (You’d need to configure this in the Room object)

To read more about socket.io, you can visit the official documentation.

If you enjoy reading this article, you can consider buying me a coffee.

Also Published here


Written by zt4ff | I love learning efficient technologies and writing about them
Published by HackerNoon on 2022/11/06