Programming a Real-time Chatting Room

2tix
13 min readMay 22, 2023

--

Introduction

Hello and welcome to a tutorial on writing realtime chat applications with Node.js and Svelte framework. In this tutorial we will be building a simple chat application that allows users to send messages to each other in realtime. We will be using Node.js and Svelte framework to build the application. We will also be using Socket.io to handle the realtime communication between the server and the client. The application will be built in a way that it can be easily extended to support multiple chat rooms and multiple users. If you would like to access the source code of the application, you can find it on Github — client and GitHub — server. If you would like to see the application in action, you can find it on its website (the server might actually take a while to load — sometimes up to a minute).

How Is It Going To Work?

Server

The server will be a Node.js application that will be responsible for handling the realtime communication between the client and the server. The server will have a list of all the connected clients as a variable in the RAM (when we shut down the server, it will disappear), and it will also have a list of all the messages that have been sent by the users (also in RAM). It will have two functions (actually not functions — will be explained later) available to the client: sendMessage and getMessages. The sendMessage function will be used by the client to send a message to the server, and the getMessages function will be used by the client to get all the messages that have been sent by the users. The server will also have a function that will be called when a new client connects to the server. This function will add the client to the list of connected clients and will also send the client a list of all the messages that have been sent by the users. The server will also have a function that will be called when a client disconnects from the server. This function will remove the client from the list of connected clients.

Client

The client will be programmed in Svelte.js with SCSS and the Vite build tool. The user will see two elements on the page, a list of messages and a form to send a message. The client will have a function that will be called when the user submits the form. This function will send the message to the server using the sendMessage function. The client will also have a function that will be called when the user connects to the server. This function will get all the messages that have been sent by the users using the getMessages function and will display them on the page. As this application should be realtime, the client will not fetch the messages at the beginning, but will have them updated when they come. The client will also have a function that will be called when the user disconnects from the server. This function will remove all the messages from the page.

Socket.io

Once again, this will be a realtime app, thus the getMessages will not be the usual REST API endpoint, which you can just fetch from the client, but will send information (the updated list of messages) to the client, without the client asking for it. This is called the bidirectional and full-duplex communication. To achieve this, we will be using Socket.io, which is a library that allows us to do this. Socket.io will be used on both the client and the server. The client will use it to send messages to the server and to get messages from the server. The server will use it to listen for messages from the client and to send messages to all the clients.

Let’s Get Started

First of all, we need to create the server part of all of this. I like to have the server and the client hosted and running on different devices (servers), so there will be a special webserver for the “server” app and a special webserver for the “client” app. If you prefer other type of architecture feel free to write into the comments for help on this, but I will be using this architecture for this tutorial.

Programming the Server

The server will be a Node.js application, so we need to create a new folder for it and initialize it with npm init. We will also need to install the dependencies: npm install express cors socket.io. We will also need to create a new file called index.js and add the following code to it:

const { Server } = require("socket.io");
const app = require("express")();
const cors = require("cors");
const port = process.env.PORT || 3000;
// process.env.PORT is for Heroku and in general for the deployment environment, 3000 is for local development (we will have to specify the port in the URL)
const messages = [];
// we will store the messages in an array (called messagesStorage in the diagram)
const clients = {};
// we will store the clients in an objects

All the explanations are given in the form of the comments, as it will be always in this tutorial. To just briefly summarize it, we are importing the socket.io package and setting up the server on a specified port. We are also creating all the storage variables we will need in the future. Now all we have left to do is the actual functionality of the server — a callback for new connections, an endpoint for receiving new messages and something that sends the updated messages to all the users. We achieve the first part (connect and disconnect) in this code:

const { Server } = require("socket.io");
const app = require("express")();
const cors = require("cors");
const port = process.env.PORT || 3000;
// process.env.PORT is for Heroku and in general for the deployment environment, 3000 is for local development (we will have to specify the port in the URL)
const messages = [];
// we will store the messages in an array (called messagesStorage in the diagram)
const clients = {};
// we will store the clients in an objects
const generateCoolNames = (adjectives, nouns) => {
const coolNames = [];
for (let adjective of adjectives) {
for (let noun of nouns) {
const coolName = `${adjective} ${noun}`;
coolNames.push(coolName);
}
}
return coolNames;
};
const adjectives = ["Black", "Silver", "Crimson", "Midnight", "Azure", "Sapphire", "Golden", "Amethyst", "Emerald", "Ruby", "Obsidian", "Jade", "Onyx", "Topaz", "Steel", "Electric", "Cobalt", "Platinum", "Copper", "Velvet"];
const nouns = ["Giraffe", "Falcon", "Phoenix", "Tiger", "Dragon", "Wolf", "Lion", "Hawk", "Panther", "Raven", "Serpent", "Fox", "Bear", "Eagle", "Leopard", "Cheetah", "Jaguar", "Puma", "Ocelot", "Cougar"];
let coolNames = generateCoolNames(adjectives, nouns);
// all with the coolNames is just to give a random name to the user (we could do it with IDs, but this is more fun)
app.use(cors());
const io = new Server(app, {
cors: {
origin: "*", // also cors
methods: ["GET", "POST"], // Specify the allowed HTTP methods
},
});
// we crate a socket.io server with the help of http
io.on('connection', socket => {
// called when a new client connects
clients[socket.id] = coolNames.pop(); // also optional
console.log(`We have a new connection, from ${clients[socket.id]}!`);
socket.emit("you", clients[socket.id]);
socket.emit("messages", messages);
// we send all the current messages to the newly connected client
socket.on("disconnect", () => {
console.log(`Client ${clients[socket.id]} disconnected!`);
coolNames = […coolNames, clients[socket.id]];
delete clients[socket.id];
});
});
app.listen(port);
console.log(`Server up and running on ${port}`);
// we pass the port to the listen method of the http server on which the socket.io is running

Now we need to add the endpoint for receiving new messages and sending them. We will do it like this (add it into the io.on(‘connection’, socket => { callback):

socket.on("sendMessage", message => {
// called when a client sends a message
console.log(`Client ${clients[socket.id]} sent a message: ${message.text}`);
messages.push({
…message,
author: clients[socket.id]
});
// we add the message to the messages array
io.emit("messages", messages);
// we send the updated messages to all the clients (io.emit sends to all the clients, socket.emit sends to the client that sent the message)
});

The code should finally look something like this:

const { Server } = require("socket.io");
const app = require("express")();
const cors = require("cors");
const port = process.env.PORT || 3000;
// process.env.PORT is for Heroku and in general for the deployment environment, 3000 is for local development (we will have to specify the port in the URL)
const messages = [];
// we will store the messages in an array (called messagesStorage in the diagram)
const clients = {};
// we will store the clients in an objects
const generateCoolNames = (adjectives, nouns) => {
const coolNames = [];
for (let adjective of adjectives) {
for (let noun of nouns) {
const coolName = `${adjective} ${noun}`;
coolNames.push(coolName);
}
}
return coolNames;
};
const adjectives = ["Black", "Silver", "Crimson", "Midnight", "Azure", "Sapphire", "Golden", "Amethyst", "Emerald", "Ruby", "Obsidian", "Jade", "Onyx", "Topaz", "Steel", "Electric", "Cobalt", "Platinum", "Copper", "Velvet"];
const nouns = ["Giraffe", "Falcon", "Phoenix", "Tiger", "Dragon", "Wolf", "Lion", "Hawk", "Panther", "Raven", "Serpent", "Fox", "Bear", "Eagle", "Leopard", "Cheetah", "Jaguar", "Puma", "Ocelot", "Cougar"];
let coolNames = generateCoolNames(adjectives, nouns);
// all with the coolNames is just to give a random name to the user (we could do it with IDs, but this is more fun)
app.use(cors());
const io = new Server(app, {
cors: {
origin: "*", // also cors
methods: ["GET", "POST"], // Specify the allowed HTTP methods
},
});
// we crate a socket.io server with the help of http
io.on('connection', socket => {
// called when a new client connects
clients[socket.id] = coolNames.pop(); // also optional
console.log(`We have a new connection, from ${clients[socket.id]}!`);
socket.emit("you", clients[socket.id]);
socket.emit("messages", messages);
// we send all the current messages to the newly connected client
socket.on("disconnect", () => {
console.log(`Client ${clients[socket.id]} disconnected!`);
coolNames = […coolNames, clients[socket.id]];
delete clients[socket.id];
});
socket.on("sendMessage", message => {
// called when a client sends a message
console.log(`Client ${clients[socket.id]} sent a message: ${message.text}`);
messages.push({
…message,
author: clients[socket.id]
});
// we add the message to the messages array
io.emit("messages", messages);
// we send the updated messages to all the clients (io.emit sends to all the clients, socket.emit sends to the client that sent the message)
});
});
app.listen(port);
console.log(`Server up and running on ${port}`);
// we pass the port to the listen method of the http server on which the socket.io is running

We can now run the server by running node index and hoping it will work, or for future uses by adding ”start”: “node index”, into the scripts field of the package.json and then run npm run start. If everything goes right, we should see Server up and running on 3000 message, we can even try to access our newborn server in the browser on localhost:3000, but when we try to do that it fails. This is because the very simple GET request made by the browser isn’t suited for our Socket architecture and in fact, we don’t have any AJAX endpoints, which the browser could use.

Let’s Get to the Client

As long as we don’t want our users, to write their own programs to connect to the server and chat with other, we should also make a nice-looking UI for our clients. We will achieve this with the help of Svelte and preferably SCSS (basically CSS on steroids). We will start by creating a new Svelte project with Vite, now if you want to do this manually, you can just create a new Vite project, choose Svelte when it asks you and then setup SCSS. If you, however, prefer everything to be done for you, you can just clone a repo I prepared for you with this command:

git clone https://github.com/2tix/svelte-template.git

Now we need to install the dependencies, so we run npm install and then we can start the development server with npm run dev. We can now open the browser on localhost:5173 and we should see a blank page without any errors. We can now start working on the UI. We will start by creating a new component called ChatComponent.svelte in the src/lib/components/chat folder. We will also create a new file called ChatComponent.scss in the src/lib/components/chat folder. We will start by adding some basic HTML into the Chat.svelte file:

<script>
import "./ChatComponent.scss";
// we import the scss file here so that it is bundled with the component
let messages = [{
text: "Hello",
time: "12:00",
author: "Cute Giraffe"
},
{
text: "Hi",
time: "12:01",
author: "you"
},
{
text: "How are you?",
time: "12:02",
author: "Cute Giraffe"
}];
// some dummy data
</script>
<div class="chat-component">
<div class="chat">
<div class="messages">
{#each messages as message}
<! - We go through every message in the storage →
<div class="message {message.author === 'you' ? 'right' : ''}">
<! - We add the class "right" to the message if it is from the user →
<div class="message-text">{message.text}</div>
</div>
<div class="message-meta {message.author === 'you' ? 'right' : ''}">{(message.author === "you" ? message.time : " - " + message.author + ` (${message.time})`)}</div>
<! - We add the class "right" to the message meta if it is from the user, we also don't show the author of the message if its our current user →
{/each}
</div>
</div>
<div class="input">
<input placeholder="Type your message here…" type="text"/>
<button><img width="20px" src="arrow.svg" alt="Send"/></button>
</div>
</div>

In this code, we create a list of messages just like you know from SMS, WhatsApp, or whatever you use. For each of the messages, we display its text, author (if it’s not the current user) and the time it was sent (currently we won’t display the date, as this is not an equivalent to WhatsApp, but rather a realtime chatting room, without permanent message history). In the field for submitting user’s desired message we use a svg image (an arrow), this you can either find in the GitHub repo I mentioned at the beginning in the public directory, or download it from Google Drive and put it into the public dir. Now let’s also add some basic styling to the ChatComponent.scss file:

.chat-component {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
.chat {
flex: 1;
overflow-y: scroll;
padding: 10px;
background-color: transparent;
border-radius: 15px;
&::-webkit-scrollbar {
width: 0;
}
.messages {
display: flex;
flex-direction: column;
.message {
display: flex;
flex-direction: column;
padding: 10px;
width: fit-content;
margin-bottom: 10px;
justify-content: center;
border-radius: 5px;
background: linear-gradient(45deg, #342679, #1a37a6);
&.right {
align-self: flex-end;
background: linear-gradient(45deg, #1a37a6, #342679);
}
.message-text {
font-size: 24px;
font-weight: 500;
}
}
.message-meta {
color: #9e9e9e;
text-align: left;
&.right {
text-align: right;
}
}
}
}
.input {
display: flex;
padding: 10px;
width: 100%;
input {
width: 100%;
padding-right: 50px;
padding-left: 10px;
flex: 1;
border-radius: 5px;
font-size: 17px;
font-weight: 500;
border: none;
outline: none;
}
button {
display: flex;
justify-content: center;
margin-left: -50px;
width: 50px;
padding: 10px;
font-size: 24px;
font-weight: 500;
outline: none;
cursor: pointer;
background: transparent;
border: none;
border-radius: 0 5px 5px 0;
img:focus-visible {
outline: none;
}
&:hover {
background-color: rgba(245, 245, 245, 0.06);
border: none;
outline: none;
}
}
&:focus-visible {
border: none;
outline: none;
}
}
}

Here is just some very basic styling just so our app looks nice, and we can see what we are doing. Now we can go back to the App.svelte file and import our new component:

<script>
import ChatComponent from "./lib/components/chat/ChatComponent.svelte";
</script>
<main>
<ChatComponent/>
</main>

Connecting to the server

Now we have a nice UI, but it doesn’t do anything. We need to connect it to the server. We will do this in a special file lib/utils/server.js:

import {io} from "socket.io-client";
import {messagesStore} from "./store.js";
// we import the socket.io client
let socket;
let you = "";
// we will use this for all our communication with the server
export const connect = () => {
socket = io("http://localhost:3000/");
// we connect to the server (when you deoploy, don't forget to change the URL (domain, port, but also protocol (http/https)))
socket.on("you", res => {
you = res;
});
// we get the name of the user from the server
socket.on("messages", res => {
messagesStore.set(res.map(message => {
if(message.author === you) {
return {
…message,
author: "you"
};
} else {
return message;
}
}));
});
};
// this function will be called when the app starts
export const sendMessage = message => {
socket.emit("sendMessage", {
text: message,
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
});
};
// this function will be called when the user sends a message

In this code we use the Svelte store which we haven’t programmer yet, but there is nothing easier we could do than just create a simple store for our messages. We will do this in the lib/utils/store.js file:

import {writable} from "svelte/store";
export const messagesStore = writable([]);

Now the last step we have to take is dealing with all of this in our ChatComponent.svelte file, where we will first import the functions we need:

import { onMount } from "svelte";
// we import the onMount function from svelte, which is called when the component is mounted
import { messagesStore } from "../../utils/store.js";
// we will use the messagesStore to get the messages when they arrive
import { connect, sendMessage } from "../../utils/server.js";
// we import the connect and sendMessage functions from the server.js file

Then we will connect to the server and subscribe to the messagesStore, right when the component is mounted:

onMount(() => {
// we call the onMount function, which is called when the component is mounted
connect();
// we call the connect function, which connects to the server
messagesStore.subscribe(value => {
// we subscribe to the messagesStore, which is updated when a new message arrives
messages = value;
// we set the messages variable to the new value
});
});

Now that we have actual messages to show we can delete the dummy data we created to see if the UI works. We will also add a function to send a message when the user clicks the send button (also when the Enter key is pressed):

<input placeholder="Type your message here…" on:keypress={e => {
if(e.key === "Enter" && e.target.value !== "" && e.target.value.trim() !== "") {
// we check if the user pressed enter and if the input is not empty
sendMessage(e.target.value);
e.target.value = "";
}
}} type="text"/>
<button><img width="20px" src="arrow.svg" on:click={e => {
if( e.target.value !== "" && e.target.value.trim() !== "") {
// we check if the input is not empty
sendMessage(e.target.parentNode.parentNode.firstChild.value);
e.target.parentNode.parentNode.firstChild.value = "";
}
}} alt="Send"/></button>

The End

Now we can test our app. We will start the server with npm run dev and then we will open two tabs in our browser (both localhost:5173). Now we can send messages from one tab to the other and vice versa. We can also open more tabs with different names and send messages between them. We can also open the app on our phone and send messages from there. The app is now fully functional and if you want, you can deploy it to a server and use it with your friends. If you have any problems, you can check out the on the link at the top of the page, or write in the comments. Thank you for reading this tutorial and I hope you learned something new.

--

--

No responses yet