d
WE ARE EXPERTS IN TECHNOLOGY

Let’s Work Together

n

StatusNeo

Guide to Code Best Practices in Node.js & MongoDB Project

In today’s fast-paced development environment, structuring your Node.js & MongoDB project efficiently is crucial for ensuring maintainability, scalability, and ease of development. This blog post offers a comprehensive guide on organizing your Node.js & MongoDB project according to industry best practices. We focus on Monolithic Architecture, consolidating all modules together. You’ll discover an optimal folder structure, key files, and code snippets that demonstrate effective implementation strategies.

Why Project Structure Matters

Before delving into the specifics, it is essential to understand the significance of a well-defined project structure:

  • Maintainability: A clear project structure simplifies the process of locating and updating different parts of your application. This is especially crucial as your project expands.
  • Collaboration: In a team environment, a standardized structure enables team members to easily find specific code and resources.
  • Scalability: A well-organized project can be extended effortlessly without adding unnecessary complexity.

Node.js & MongoDB project

Project Structure Overview

A well-structured Node.js project typically includes the following key elements:

Two main folders: app and bin

Essential root files such as server.js, security.js, swagger.js, .env, .pm2.json, package.json, and .gitignore.

You can download full code from GitHub Code Repo

 

 

Detailed Breakdown of Folders and Files

1. app Folder

This folder contains the core components of the application, segregating different responsibilities into various subfolders.

  • Routes (app/routes): Define API endpoints.

              routes.js: A dynamic function to handle route definitions, making the routing process maintainable without the need to manually add routes. Controllers (app/modules/user): Handle incoming requests and return responses.

  • Controllers (app/modules/user): Handle incoming requests and return responses.

              user.controller.js: Implements the Single Responsibility Principle (SRP) by ensuring each function handles a specific part of the request-response cycle.


const path = require('path');
const { createToken } = require(path.join(__basedir, "app", "middlewares"));
const { Response } = require(path.join(__basedir, 'app', 'common', 'Common'));
const { constants, messages } = require(path.join(__basedir, 'app', 'config'));
const User = require(path.join(__basedir, 'app', 'database', 'operations', 'User'));
const { SUCCESS, ERROR } = constants;
/**
 * Controller to get user data by id
 * @param {object} req HTTP request object
 * @param {object} res HTTP response object
 * @param {function} next next method
 */
const login = async (req, res, next) => {
    try {
        const token = await createToken({ userId: req.currentUser._id });
        Response.commonResponse(res, SUCCESS, messages.DATA_FOUND, { token });
        next();
    } catch (error) {
        console.error(`${messages.ERROR_USER}: `, error);
        return Response.commonResponse(res, ERROR.INTERNAL_SERVER_ERROR, messages.ERROR, error);
    }
};

const saveData = async (req, res, next) => {
    try {
        await dummyData();
        Response.commonResponse(res, SUCCESS, messages.DATA_SAVED);
        next();
    } catch (error) {
        console.error(`${messages.ERROR_SALES}: `, error);
        return Response.commonResponse(res, ERROR.INTERNAL_SERVER_ERROR, messages.ERROR, error);
    }
};
module.exports = {
    login,
    saveData,
};
    
  • Services (app/modules/user): Encapsulate business logic.

             user.service.js: Contains business logic functions related to user operations.


 const dummyData = async () => {
    //This is just sample, please avoid using any hardcoded ID
     let data = {
         email:"[email protected]",
         roleId:ObjectId('1234abcd678')
     }
     return await User.insertData(data);
 };
  • Module Route (app/modules/user):

               index.js: A route file where we will define all routes for module


module.exports = router => {
     /**
      * @swagger
      * /users:
      *  get:
      *      tags:
      *          - USERS
      *      security:
      *      - JWT: []
      *      summary: Get all users
      *      description: Returns all users
      *      produces:
      *          - application/json
      *      responses:
      *          200:
      *              description: An array of User Objects
      *              schema:
      *                  type: array
      *                  items:
      *                      $ref: '#/definitions/users'
      *          400:
     *              description: Bad Request
     *          401:
     *              description: Authentication Failed
     *          403:
     *              description: Not Authorized
     *          404:
     *              description: Not Found
     *          500:
     *              description: Internal Error
      *
      */
     router.get("/user-refresh", authenticateUserWithToken, checkRoleAccess, refreshUser);
  • Operations (app/operations): Database operation layer.

               User.js: Extends the base operation layer and dynamically sets the model, reducing the amount of code in service files. This abstraction enhances code quality by centralizing common functionality and promoting a cleaner, more maintainable codebase.


class User extends Base {
    constructor() {
        super()
        this._modelName = Schema.USER;
        super.initialize(this)
    }
    /**
     * Returns all the user from database.
     *
     *
     * @since      1.0.0
     * @access     public
     *
     *
     * @alias    getUserData
     * @memberof UserClass
     *
     * @param offset sets the offset
     * @param limit sets the limit
     * @return {Promise} Transactions Data
     */
    async getUserData(fieldKey, fieldValue) {
        let query = (fieldKey) ? { [fieldKey]: fieldValue } : {};
        return await this._getOne(query);
    }

    async getData (limit = 10, offset = 0) {
        return await this._get(false,false,false,false,limit);
      }
    
      async insertData (data) {
        return await this._create(data);
      }
}

module.exports = new User();

               Base.js: Contains common database functions and a dynamic model initialization constructor. A Base layer operation assists developers in consolidating all database queries into a single file. This allows for a unified function to handle specific types of database operations across the entire application. An additional advantage of using this Base layer is the ease of switching databases. If a database change is required, modifications are limited to a few files, eliminating the need to update queries in all service and controller files.

                    – Use of a constructor and dynamic model initialization.


class Base {
  constructor() {
    this.model = null;
  }

  initialize(self) {
    this._modelName = self._modelName;
  }

                    – A dynamic function for various get queries.


 async _get(_lookup, _unwind, _match, _sort, _limit, _skip) {
    const aggregate = [];
    if (_lookup) {
      const lookup = {
        $lookup: _lookup
      }
      aggregate.push(lookup);
    }
    if (_unwind) {
      const unwind = {
        $unwind: _unwind
      }
      aggregate.push(unwind);
    }
    if (_match) {
      const match = {
        $match: _match
      }
      aggregate.push(match);
    }
    if (_sort) {
      const sort = {
        $sort: _sort
      }
      aggregate.push(sort);
    }
    if (_limit) {
      const limit = {
        $limit: _limit
      }
      aggregate.push(limit);
    }
    if (_skip) {
      const skip = {
        $skip: _skip
      }
      aggregate.push(skip);
    }
    return await dbConnection.dbInstance[this._modelName].aggregate(aggregate);
  }

                    – Functions for different database queries.


async _getOne(query){
    return await dbConnection.dbInstance[this._modelName].findOne(query);
  }

  async _create(data){
    let dataInstance = new dbConnection.dbInstance[this._modelName]();
    dataInstance = Object.assign(dataInstance, data);
    return await dataInstance.save();
  }

   // Bulk Create
   async _bulkCreate (fieldsArray) {
    return await dbConnection.dbInstance[this._modelName].bulkCreate(fieldsArray);
  }

  async _update (query, data) {
    return await dbConnection.dbInstance[this._modelName].updateOne(query, { $set: data });
  }

  async _updateMany(query, data){
    return await dbConnection.dbInstance[this._modelName].updateMany(query, { $set: data });
  }

  async _delete (query) {
    return await dbConnection.dbInstance[this._modelName].deleteOne(query);
  }

  async _deleteMany (query) {
    return await dbConnection.dbInstance[this._modelName].deleteMany(query);
  }
  • Response (app/common): Common function for response

               Common.js: Defines a class of Response


class Response {
  /**
   * Returns common standard for post/get request.
   *
   * Add status, message with response.
   *
   * @since   1.0.0
   * @access  public
   *
   *
   * @alias    commonResponse
   * @memberof CommonClass
   *
   *
   * @return {Json} status, message and response
   * @param {Object} res for http response
   * @param {Number} status for http response code
   * @param {String} message for http response message
   * @param {Object} responseData for http response data
   * @param [instance]
   */
  static commonResponse (res, status, message, responseData = null) {
    return res.status(status).send({
      'status': status,
      'message': message,
      'response': responseData || []
    })
  }
}

module.exports = { Response }
  • Utilities (app/config): Common helper functions.

                constants.js: Defines application-wide constants.

                messages.js: Common messages used throughout the application.

                database-schema.js: Defines table names to avoid spelling errors.

  • Database (app/database/bootstrap): Manages database connections and configurations.

               dbConnection.js: Manages MongoDB connections and synchronizes models. After establishing a connection with the database, we utilize a single instance for the entire application. This approach helps reduce the database load caused by multiple connections. Within this single instance, a connection pool is implemented, which automatically releases connections as needed. This ensures a consistent and efficient use of a single database instance throughout the application.


 const mongoose = require("mongoose");
 const fs = require("fs");
 const path = require("path");
 const { constants, messages } = require(path.join(__basedir, 'app','config'));
 const modelPath = path.normalize(path.join(__basedir, 'app','database','models'));
 const { MONGO_URI } = constants;
 const dbConnection = {}, db = {};

/**
 * Method for connecting to mongoDB
 * */

const connectToMongoDb = async () => {
    mongoose.connection.on("connected", () => {
        console.log(`${messages.MONGODB_CONNECTED}`);
    });
    mongoose.connection.on("error", (err) => {
        console.log(`${messages.MONGODB_CONNECTION_ERROR}: ${err}`);
    });
    mongoose.connection.on("disconnected", () => {
        console.log(`${messages.MONGODB_DISCONNECTED}`);
    });

    let connect = await mongoose.connect(MONGO_URI, {
        useNewUrlParser: true,
        useUnifiedTopology: true
    });

    // loop through all files in models directory

    let modelData = fs.readdirSync(modelPath);
    for (const file of modelData) {
        let modelNames = file.split('.');
        const model = connect.model(modelNames[0], require(path.join(modelPath, file)));
        db[model.collection.collectionName] = model;
    }
    dbConnection.dbInstance = db;
};

module.exports = {
    connectToMongoDb,
    dbConnection
};
  • Models (app/database/models): Define data models using schemas.

               user.js: A sample model for user data.

  • Middleware (app/middleware): Custom middleware functions.

                auth.js: Middleware for handling authentication and authorization.


const authenticateUserWithToken = async (req, res, next) => {
    try {
        const auth = req.headers.authorization;
        if (!auth) {
            console.log(messages.ACCESS_DENIED);
            return Response.commonResponse(res, constants.ERROR.BAD_REQUEST, messages.ACCESS_DENIED, error);
        }
        const authParts = auth.split(" ");
        if (authParts.length !== 2) {
            console.log(messages.TOKEN_FORMAT);
            return Response.commonResponse(res, constants.ERROR.BAD_REQUEST, messages.TOKEN_FORMAT, error);
        }
        const [scheme, token] = authParts;
        if (new RegExp("^Bearer$").test(scheme)) {
            try {
                const { userId }  = await verify(token, SECRET);
                let user = await User.getUserData('_id', userId);
                req.currentUser = user;
                commonValidator(req, res, next)
                // next();
            } catch (e) {
                console.log(`${messages.ERROR_AUTH}: `, e.message);
            }
        } else {
            console.log(messages.TOKEN_FORMAT);
            return Response.commonResponse(res, constants.ERROR.UNAUTHENTICATED, messages.TOKEN_FORMAT, error);
        }
    } catch (error) {
        return Response.commonResponse(res, constants.ERROR.UNAUTHENTICATED, messages.ERROR, error);
    }
};

2. bin Folder

Contains the startup script for the HTTP server.
               www: Initializes and starts the HTTP server, handling errors and setting the server port configuration.


const path = require("path");
const { createServer } = require("http");
const { app } = require("../server");
const { constants } = require(path.join(__basedir, "app", "config"));
const { PORT } = constants;
const server = createServer(app);

// Event listeners to catch uncaught errors
process.on("unhandledRejection", (error) => {
  console.log(error.message, { time: new Date() });
  process.exit(1);
});

process.on("exit", (code) => {
  console.log(`Exiting with code: ${code}`);
});

server.listen(PORT, (err) => {
  if (err) {
    return console.log(`Something went wrong: \n${err}`);
  }
  console.log(`Server is listening on port: ${PORT}`);
});

3. Root Files

                 server.js: Initializes the Express application, sets up middleware, rate limiters, routes, and handles timeouts.
                        – Import all dependencies as required for project in with express.
                        – Put express configuration which required to project
                        – Call all function and classes to bind the project features
                        – Put rate limiter for api calling and bind all routes
                        – Handle timeout

               security.js: Security configurations such as setting up helmet for securing HTTP headers.
               swagger.js: Sets up Swagger for API documentation.
               .env: Environment variables.
              .pm2.json: PM2 configuration file for managing and monitoring the application.
              package.json: Lists project dependencies and scripts.
              .gitignore: Specifies files and directories to be ignored by Git.

By adhering to this structure, you ensure that your Node.js project is organized, maintainable, and scalable. This approach not only streamlines development but also facilitates collaboration and future expansion. Implement these best practices to build robust and efficient Node.js applications.