DEV Community

Cover image for Building Robust Node.js Applications with Socket.io: Best Practices
Olasunkanmi Igbasan
Olasunkanmi Igbasan

Posted on • Updated on

Building Robust Node.js Applications with Socket.io: Best Practices

Introduction

This article will guide you through building a basic AI chatbot using Node.js and socket.io while incorporating some of the best practices for Node.js development. With its bi-directional communication capabilities, socket.io makes it easy to build real-time applications that require continuous interaction between the server and clients.

Throughout the development process, we'll follow best practices to ensure that our application is secure, scalable, and maintainable.

Setting up the Environment

To set up the environment for building a web application using Node.js, you will need to follow a few simple steps:

1 Install Node.js.
2 Setup project directory.

my-chatbot/
├── node_modules/
├── public/
│   ├── css/
│   │   ├── style.css
│   ├── js/
│   │   ├── chat.js
│   │   ├── socket.io.js
│   ├── index.html
├── app/
│   ├── services/
│   |     ├── store.js
│   ├── socket/
│   |    ├── socket.js
│   |    ├── socketHelper.js
│   ├── utils/
│   |     ├── constants.js
├── config/
│   |      ├── app.js
├── app.js
├── .env
├── .gitignore
├── package-lock.json
├── package.json
└── README.md

Enter fullscreen mode Exit fullscreen mode

3 Install the necessary dependencies.

npm i express socket.io
Enter fullscreen mode Exit fullscreen mode

Socket.io

Socket.io is a popular library for building real-time web applications that require bi-directional communication between the server and clients. It is built on top of the WebSockets API and provides a simple and elegant API for building real-time applications with features such as broadcasting, namespaces, and rooms.

Socket.io would be a crucial part of the chatbot has it would majorly be handling the communication between our server and client.

Creating the express server

app/: This directory contains all the backend code for our application.

Within this file we would be initializing our express application.

const express = require('express');
const { appConfig, Logger } = require('./config');
const app = express();

global.logger = Logger.createLogger({ label: 'E-commerce Chatbot' });

appConfig(app);

module.exports = app;

Enter fullscreen mode Exit fullscreen mode

This is a simple configuration that exports an instance of the Express application after initializing the app configuration and logger.

Firstly, we import the express framework module, then we import appConfig, Logger from the config module.

global.logger = Logger.createLogger({ label: 'E-commerce Chatbot' });
Enter fullscreen mode Exit fullscreen mode

In the line above, logger is defined as a global variable. This allows it to be accessed from any module within the Node.js application, providing a centralized location for logging.

Configuring Express app

const express = require('express');
const http = require('http');
const path = require('path');
const Socket = require('../app/socket/socket');

const appConfig = async (app) => {
    // Set up server
    const server = http.createServer(app);

    // Set up socket
    const { SocketInstance } = Socket.createSocket(server);
    SocketInstance(server);
    // Set static files
    app.use(express.static(path.join(__dirname, '../public')));

    const PORT = 9000;
    // Listen to port
    server.listen(PORT, () => {
        logger.info(`Server running on port ${PORT}`);
    });
};

module.exports = appConfig;

Enter fullscreen mode Exit fullscreen mode

This is the appConfig function which configures the Express app.

The appConfig function plays a critical role in configuring the Express app for the chatbot. It creates an HTTP server using http.createServer(app), configures the socket instance using Socket.createSocket(server), and sets up static files with express.static(). The function then listens to a specified port using server.listen(). These actions follow Node.js best practices, such as setting up the server to listen on a specific port and using the path module to serve static files.

Additionally, it is a good practice to keep the app configuration within the scope of a function in Node.js. It helps to isolate the configuration code and prevents it from being accessed or modified outside of the function's scope, which can help improve the security of the application.

By doing so, you can limit the access to important variables or configurations that are necessary for your application to work properly, making it more difficult for unauthorized access or modification. Additionally, it can also help to improve the maintainability of your code by keeping it organized and easy to read.

Configuring Socket.io

One of the key aspects of backend development is data-privatization and encapsulation. One of best way to implement this is the use of a class-based approach.

Within the Socket class, private methods are used to handle the socket events, such as when a user sends a message or when a user joins or leaves the chatroom. The use of private methods helps to keep the implementation details hidden from external modules, improving the overall security and maintainability of the code.

Furthermore, the public methods of the Socket class are used to emit messages to the chatroom, whether it be from the user or the bot. By separating these methods, it becomes easier to manage the logic of the socket instance and improves the overall readability of the code.

The constructor of the Socket class initializes the instance with the necessary properties, such as the server and the socket instance. Additionally, default values for the bot and user names are set, providing a more customizable implementation.

The initializeSocket method is responsible for handling the connection event and initializing the necessary event listeners for user messages, bot messages, notifications, and disconnections. By organizing these events in a separate method, the code is easier to understand and maintain.

Finally, the createSocket static method is used to create a new instance of the Socket class and initialize the socket instance. This method provides an easy-to-use interface for other modules to create and manage their own socket instances.

*Socket config 👇🏼 *

const socket = require('socket.io');
const constants = require('../utils/constants');

const {
    CHAT_BEGINNING,
    NEW_CONNECTION,
    BOT_NAME,
    BOT_INTRO,
    BOT_INTRO_2,
} = constants;

class Socket {
    constructor(server) {
        this.server = server;
        this.io = socket(server, { cors: { origin: '*' } });
        this.botName = BOT_NAME;
        this.userName = 'User';
    }

    /**
     * @private
     * @function _emitUserMessage
     * @param {object} socket
     * @param {object} message
     * @emits userMessage - when a user sends a message
     * @memberof Socket
     * @returns {object} message - returns user message to the chatroom
     */
    _emitUserMessage(socket, message) {
        socket.emit('userMessage', Helper(this.userName, message));
    }

    /**
     * @private
     * @function _emitBotMessage
     * @param {object} socket
     * @param {object} message
     * @emits botMessage - when the chatbot sends a message
     * @memberof Socket
     * @returns {object} message - returns bot message to the chatroom
     */
    _emitBotMessage(socket, message) {
        socket.emit('botMessage', Helper(this.botName, message));
    }

    /**
     * @private
     * @function _emitNotification
     * @param {object} socket
     * @param {object} message
     * @emits notification - when a user joins or leaves the chatroom
     * @memberof Socket
     * @returns {object} message - returns notification to the chatroom
     */
    _emitNotification(socket, message) {
        socket.emit('notification', Helper(this.botName, message));
    } 

    /**
     * @private
     * @function _broadcastNotification
     * @param {object} socket
     * @param {object} message
     * @emits notification - when a user joins or leaves the chatroom
     * @memberof Socket
     * @returns {function} _emitNotification - returns notification to the chatroom
     */
    _broadcastNotification(socket, message) {
        this._emitNotification(socket.broadcast, message);
    }

    /**
     * @private
     * @function _emitDisconnect
     * @param {object} socket
     * @emits disconnect - when a user disconnects from the chatroom
     * @memberof Socket
     * @returns {function} _broadcastNotification - returns notification to the chatroom
     */
    _emitDisconnect(socket) {
        socket.on('disconnect', (reason) => {
            this._broadcastNotification(socket, reason);
        });
    }


    /**
     * @private
     * @function initializeSocket
     * @memberof Socket
     * @returns {function} _listenRegister - listens for register
     * @returns {function} _emitNotification - emits notification to the chatroom
     * @returns {function} _emitBotMessage - emits bot message to the chatroom
     * @returns {function} _listenToChatMessage - listens for chat message
     * @description Initializes socket
     * @listens for connection
     * @emits userMessage
     * @emits botMessage
     * @emits notification
     * @emits disconnect
     * @emits access
     */
    initializeSocket() {
        this.io.on('connection', (socket) => {
            logger.info(NEW_CONNECTION(socket.id));


            // Emit to the new user only
            this._emitNotification(socket, CHAT_BEGINNING);

            // Emit bot message
            this._emitBotMessage(socket, BOT_INTRO);

            this._emitBotMessage(socket, BOT_INTRO_2);


        });


    }

    /**
     * @static
     * @function createSocket
     * @param {object} server- server instance
     * @memberof Socket
     * @returns {object} socketInstance - returns socket instance
     * @description Creates a socket instance
     */
    static createSocket(server) {
        const _createSocketInstance = (server) => {
            const socketInstance = new Socket(server);
            return socketInstance.initializeSocket();
        };

        return {
            SocketInstance: _createSocketInstance,
        };
    }
}

module.exports = Socket;
Enter fullscreen mode Exit fullscreen mode

Full Break down

When the server starts up, the Socket constructor is called and is passed the HTTP server instance. Inside the constructor, the Socket.IO library is used to create a new socket instance and this instance is stored as a property of the Socket object. The Socket object also initializes some properties to store the names of the bot and the user, which will be used later.

The Socket class has several private methods that are used to emit messages to the connected sockets. These methods include _emitUserMessage, _emitBotMessage, _emitNotification, and _broadcastNotification. These methods all take a socket object and a message object as parameters and emit the message to either the socket that called the method or to all sockets except for the socket that called the method.

Advantages of using private class methods

  • Defining these methods as private methods within the class provides encapsulation and modularity to the code. By keeping these methods within the class, they are not exposed to the global scope and can only be accessed within the class itself. This prevents potential naming conflicts with other methods or variables in the global scope.

  • Additionally, by encapsulating these methods within the class, it makes it easier to maintain and modify the code. Since these methods are closely related to the functionality of the class, it makes sense to keep them within the class for better organization and ease of access.

  • Another advantage of defining these methods as private within the class is that it helps to abstract away some of the complexities of using the Socket.io library. By creating methods that handle the details of emitting specific events and messages, the rest of the code can focus on the higher-level logic of the application, without needing to worry about the specifics of Socket.io's API.

  • Finally, it also helps with code reusability, as these methods can be called from within other methods in the class, rather than being duplicated across multiple functions. This helps to reduce code duplication and makes it easier to maintain the codebase.

The Socket class also has a public method called initializeSocket, which is called to start the socket server. Inside this method, the Socket.IO library's on method is used to listen for the connection event. When a new client connects to the socket server, a new socket object is created for that client and a logger is used to log that a new connection has been established.

The initializeSocket method then calls some of the private methods to send messages to the client, such as a welcome message from the bot, a notification that a new user has joined, and an introduction message from the bot. The method also listens for the disconnect event and broadcasts a notification to all connected sockets when a client disconnects from the server.

Finally, the createSocket static method is used to create a new instance of the Socket class and initialize the socket instance. This method provides an easy-to-use interface for other modules to create and manage their own socket instances.

A class-based approach for socket.io can be particularly beneficial for backend developers, as it allows them to maintain a cleaner and more organized code structure. This is especially important for backend development, where code can become complex and difficult to manage as the application grows in size and complexity.

By encapsulating socket-related logic and events within a class, backend developers can effectively separate the concerns of the application, making it easier to manage and maintain the code. This can be particularly helpful when working with large-scale applications that require multiple developers to work on different parts of the codebase.

In addition to improving the organization of the code, using a class-based approach for socket.io can also help to prevent conflicts and improve modularity. This is because different parts of the application can use sockets without interfering with each other, allowing developers to work on different components of the application in parallel.

Furthermore, by adopting a class-based approach, backend developers can make use of object-oriented programming concepts, such as inheritance and polymorphism, which can lead to more efficient and maintainable code. For example, by using inheritance, developers can create a base class for common socket-related logic and events, and then extend this class to add specific functionality for different parts of the application.

Overall, using a class-based approach for socket.io can be a valuable tool for backend developers, helping to streamline development, improve code organization, and enhance modularity and efficiency.

Top comments (4)

Collapse
 
tobisupreme profile image
Tobi Balogun • Edited

Great article! Thank you for sharing


In the section talking about setting up environment variables, I think it should say
npm install express socket.io

Collapse
 
drvnprgrmr profile image
Ibukun Ekunwe

Nice approach to using classes. I'm more of a functional guy myself but I guess I can see the appeal.

Collapse
 
geekiedj profile image
Barbara Odozi

This is good!

It’s always functions for me but yeah I like this.

Collapse
 
olatisunkanmi profile image
Olasunkanmi Igbasan

I used functions within the class too