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 for the server-side code, listen to web socket communication using and the client-side will be developed with vanilla JavaScript. Express.js Socket.io Setting up our project We will create a directory named and change the directory to the directory using the command. chat-app $ 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 on your browser to test that the application works [http://localhost:8001/index](http://localhost:8001/index) Initializing on the server-side socket.io To initialize a socket on the server side, follow the following steps. Install dependency into our application by running the command. socket.io $ yarn add socket.io Import into our code, create a new socket server, and then add an event listener to the socket to listen if a connection is made. socket.io // 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 on the client-side socket.io 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 directory including files to build up our UI, making our project structure look like this. public chat-app/ |- node_modules/ |- public/ |- index.html |- main.js |- app.js |- package.json |- yarn.lock We are going to be making use of to style the Client UI to reduce the amount of custom CSS we’d be writing. Tailwind CSS In the , create a template for our chat window. index.html <!-- 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 on the client side and another file to write our custom JavaScript code. socket.io main.js Then in the file, we would create a function that can add a message to the chatbox. The function 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 main.js createMessage 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, and make use of static files to render the client UI. app.js // 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"); }); : 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 to automate this process for us. NOTE nodemon Install by running. nodemon $ npm install -g nodemon Then run the node application using nodemon. $ nodemon ./app.js Open on your browser to view what the chat app would look like. [http://localhost:8001](http://localhost:3000) 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 class to manage this data for us. Room We’d create a new file named in the root directory of our project. Then we create the class and have the constructor initialize a property for keeping the state of our room. room.js 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 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 would look like this. roomsState roomsState // 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 in our application. UUID Install by running this command in our terminal. uuid $ 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; : 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. NOTE We’d add another method to the class, , to reduce the number of users in a particular room. Room leaveRoom() // 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 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. leaveRoom() 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 the method just deducts the number of users in that room by one. leaveRoom() Finally, our code should be similar to this. room.js // 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. allows us to create arbitrary channels that sockets can join and leave. It can be used to broadcast events to a subset of clients. socket.io ( 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 returns a unique ID which is our unique room ID. So we can join and leave room in our rooms as follow. Room.joinRoom() // 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 = ""; } }); : 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. NOTE 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 , you can visit the official documentation. socket.io If you enjoy reading this article, you can consider . buying me a coffee Also Published here