Creating anonymous chat rooms with Socket.io and Express.js

Kayode - Apr 17 '22 - - Dev Community

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
Enter fullscreen mode Exit fullscreen mode
  • Initialize our Node application by running the command.
$ yarn init -y
Enter fullscreen mode Exit fullscreen mode
  • Install express in our project using yarn by running the command.
$ yarn add express
Enter fullscreen mode Exit fullscreen mode
  • We will create a JavaScript file, named app.js, 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")
})
Enter fullscreen mode Exit fullscreen mode
  • Now we can start the application by running the command.
$ node app.js
Enter fullscreen mode Exit fullscreen mode

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

welcome-home.png

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 is able to 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 from 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);
Enter fullscreen mode Exit fullscreen mode

Change the code in the server application, app.js 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");
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then run the node application using nodemon.

$ nodemon ./app.js
Enter fullscreen mode Exit fullscreen mode

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

chatapp-basic.png

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;
Enter fullscreen mode Exit fullscreen mode

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
    }
]
Enter fullscreen mode Exit fullscreen mode

Next, add a method to join a room. The method will loops through the room to check if any rooms have a number of 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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

The leaveRoom() 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 so as to delete that particular room state.
If the user in the room is greater than 1, the leaveRoom() 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;
Enter fullscreen mode Exit fullscreen mode

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.

rooms-dark.png

(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");
});
Enter fullscreen mode Exit fullscreen mode

In our server application, once a 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);
  });
});
Enter fullscreen mode Exit fullscreen mode

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 message 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 = "";
  }
});
Enter fullscreen mode Exit fullscreen mode

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);
  });
});
Enter fullscreen mode Exit fullscreen mode

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");
});
Enter fullscreen mode Exit fullscreen mode

And our client side JavaScript looking 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 = "";
  }
});
Enter fullscreen mode Exit fullscreen mode

Testing our chat app

To text our chat app, we will open four different browsers to confirm that two rooms are created.
ezgif.com-gif-maker (1).gif

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .