Build a Chat Web App with MERN Stack and Socket.IO

Part 1

Gil Palikaras
8 min readOct 13, 2020

This is part one of my tutorial on how to build a web application in JavaScript, using the MERN stack (MongoDB, Express, React, Node.js) and the Socket.IO library.

mongodb, express, react, nodejs, socket.io

Our goal is to build a web chat that holds a single chat room. Any user can connect to it from an open window/tab, is able to upload an image to use as avatar during the chat, each connected user will see instantly any message sent, and new connected users will see the last 10 messages sent.

Final product preview

I will also demonstrate how to use Cloudinary API to save images as the avatars of connected users in the chat.

Some key notes:

  • The web app will be consist of 2 parts, the client and the server.
  • The client will contain 2 main components: a login form, where the user can write a use name to use for the chat and the option to upload an image to use as an avatar throughout the conversation. The second component will consist of the chat messages, where each message that is sent, is viewed from anyone that’s connected to the chat room.
  • The server will accept two type of connections: an HTTP request, to accept an image file, upload to a Cloudinary media library through their API and return the image’s link. By creating an account, you can get about 100MB of storage. The second connection the server manages is a WebSocket port to manage incoming connections and broadcast messages to all connected clients, in real time.
  • As we expect a large volume of data to be written to the database in a short amount of time and the data that we save are unstructured (using an avatar is not mandatory), it makes sense to use a NoSQL type of database. For this application, we will be using MongoDB.

You can see the full code here in my GitHub.

Socket.IO and WebSocket — what’s the difference?

Most websites you visit use HTTP to make API calls, which means the client sends a request to the server, and the server sends back a response. This kind of communication can only be initiated by the client. It suits most needs for a website, e.g. to get data from a server once.

What if there are other needs? What if the client needs to check often for data? There is a solution for that, called AJAX long-polling, but it’s not efficient. Long-polling involves sending periodic HTTP requests for data, introducing latency and increasing server load.

That’s where WebSocket comes in. It’s a communications protocol that offers full-duplex communication channels over a single TCP connection. In other words, a WebSocket connection remains open as long as both the client and the server choose not to close it. While the connection is open, messages can be exchanged both ways. This allows to be used for applications like push messages to clients or real-time chat between clients.

Socket.IO is an event-driven JavaScript library that offers an abstraction layer for using bi-directional communication between web clients and servers, and provides an easy configuration of WebSocket implementation.

Setting up the project

This is the folder structure we will use in the project:

chat-app
├── client
│ └── src
│ ├── components
│ ├── index.js
│ ├── App.js
│ ├── index.html
├── server
│ └── config
│ └── controllers
│ └── models
│ ├── index.js
│ ├── start.js

Setting up the server

In our project’s root folder go to folder server and create a Node.js project using Yarn in the command line. You can still use Node’s default package manager NPM, I just noticed that yarn is faster and updates its lock file immediately (yarn.lock).

//-y is to answer yes to all questions it will prompt and keep default values
yarn init -y

This will create a package.json to manage our Node.js package and run configuration scripts.

Here is a good point to mention that we will be using Babel to write JavaScript with ES6+ coding standards, importing and exporting modules. On execution Babel will transpile the code we wrote into ES5 JavaScript that Node.js runs.

Install the packages we will be using, first in development run:

yarn add -D nodemon

and in production:

yarn add @babel/core @babel/preset-env @babel/register babel-polyfill express body-parser cloudinary dotenv mongoose multer socket.io

A little explanation about the modules we installed:

  • Nodemon: runs in development mode, whenever you make changes to your code and save, it restarts our server.
  • Babel/core: contains the Node API and requires hook.
  • Babel/preset-env: contains a set of plugins to convert ES6 features to equivalent ES5.
  • Babel/register: uses Node’s require() hook system to compile files when they are loaded.
  • Babel-polyfill: includes a custom regenerator runtime and core-js which imitates a full ES6 environment and is intended to be used in an application.
  • Express: minimal Node.js framework that uses an abstraction layer to perform operations such as creating a server and handling routes.
  • Body-parser: Body parsing middleware to parse incoming request bodies to pass to our routes.
  • Cloudinary: module that includes Cloudinary API.
  • Dotenv: module that loads production-sensitive data (database connection links, passwords, etc) in form of variables into the application.
  • Mongoose: ODM for performing transactions with MongoDB.
  • Multer: module for handling file uploads from requests.
  • Socket.io: module that helps to set up a Socket.IO server for accepting and broadcasting messages to connected clients.

In order to be able to use babel and write ES6 code in our project, create first the Babel configuration file, .babelrc in the server folder and add inside the following:

We set babel by passing the package @babel/preset-env in the property “presets”, while also letting Babel know what is the minimum version of Node.js our app supports. This will allow babel to perform the required transpilation.

In the same server folder create the file start.js. This will act as the starting point of the application. It will load the main endpoint of the server (index.js), and use Babel to transpile:

When starting the application from the scripts configuration in package.json, this is the file that will be called, bootstrapping the entire application:

Now for the main application: first create a file named .env in the server folder to save all the environment variables we will be using:

Since in the client, the first thing the user sees is the login form, where they enter their username and upload an image, we will start on how express is setup, how we handle incoming requests and how the image is uploaded.

Use of Cloudinary API

In folder controllers, create the file fileUploader.js with this code:

The function accepts the request and response objects from express as its parameters. First of all, if the user didn’t send a file. If not, return, return a response of HTTP status 204 (successful response, but there is nothing to send back in the response body).

In normal execution, initialize the Cloudinary object with connection parameters (cloudinary.config()) and upload the image sent, by reading the file from the request object as a stream. When upload is done and there are no errors, send back to the client a link with the image’s URL in the CDN.

Setting up the model and database connection

For the next part, I’ll show is how to set up connection with MongoDB and the model of the collection where the messages will be saved with Mongoose. I suggest using the cloud service of MongoDB Atlas , where it’s very easy to setup your database and connect with your application, plus they give 512MB of free storage for your data. The goal is to have a collection in MongoDB named messages that will hold our messages.

In folder models create the file message.js with this code:

Here we defined the model of the Messages collections, where we set what fields we will be saving, their format and if they are mandatory or not. We will also be saving the create time of every message, but not the update time.

In the folder config create the file mongo.js. This file creates the connection to MongoDB that is called once when the app starts and Mongoose can use to perform queries.

Setting up the HTTP server

Now, to put it all together, and see how Socket.IO interacts with our modules, create the main endpoint of the server, index.js in the server folder.

In the top, write the module imports and our constants to use.

Here we import the modules we will be using and setting up loading of environment variables with dotenv, calling express and initializing socket.io with its port number to listen for WebSocket connections.

Next, we need to add a small middleware to set what responses our server will return on each request.

Then, by using the body-parser module, set that incoming HTTP requests will have their content parsed as JSON.

In order to upload files, we need to set up Multer to save incoming files in the request in memory as a buffer before uploading to Cloudinary CDN. This way, Cloudinary can read the buffer as a stream of data, and pass the output of Multer middleware to fileUploader.js and start uploading the file.

To bootstrap the server, add this function. This will initialize the database connection and start the server to listen to the HTTP port that was defined in the .env file for incoming connections.

When calling it, add a .catch() clause in order to log any errors from file uploading, database interaction and message broadcasting.

Setting up the Socket.IO service

Now let’s move to the interesting part, setting up and handling WebSocket connections and operations!

When a user first logs in, they should be able to see the 10 last messages that were written from any user. For that, in index.js, add this function that loads them (the object parameter in sort() indicates not to return the field _id in our results):

When we listen to incoming WebSocket connections, we will be using this function with a callback that handles any operations with the incoming connection object. The parameter socket contains info on the incoming client that connects and data it might include.

Within that callback, call the function getMostRecentMessages, and when it returns results, emit them to all incoming connections with a custom tag (to be handled by the connected client). We reverse the results, because in the chat we want to see the last message in the bottom, above the message text box.

With every incoming connection (e.g. a user sent a message), we want to extract the info we want from this message (user name, user avatar and message text) and save it to MongoDB.

That’s it! To run our server, in the directory server, run this command:

yarn run start

Stay tuned for part 2 where we will learn how to build the client in React, that will send and receive messages in real time!

Final Words

I hope you’ve enjoyed my first article on Medium.
If you like this post, I would appreciate applause and sharing :-)

Be sure to follow me via Twitter, LinkedIn and Medium.

Who Am I?
I am Gil Palikaras, a Web Developer specializing in Backend Development and System Architecture Design.

--

--

Gil Palikaras

Software Developer Engineer | Blogger | Husband | Father | Runner 5K. I like to write about technology as a way to help others and to keep track of my progress