Organizing Code in Node.js Application

Table Of Contents

Node.js is a popular server-side runtime engine based on JavaScript to build and run web applications. Organizing our source code right from the start is a crucial initial step for building large applications.

Otherwise, the code soon becomes unwieldy and very hard to maintain. Node.js does not have any prescriptive framework for organizing code. So let us look at some commonly used patterns of organizing the source code in a Node.js application.

Leveraging Node.js Modules as the Unit of Organizing Code

Modules are the fundamental construct for organizing code in Node.js. A module in Node.js is a standalone set of potentially reusable functions and variables. They are imported by other applications or modules which need to use the functions defined in the imported modules.

This approach makes it easier to reuse code and maintain consistency across our application. We should use the principle of DRY when defining modules. Whenever we see a possibility of code reuse we should package them in a module. The module can be scoped to our application or could be made public.

Exporting Blocks of Reusable Code

We specify the functions and variables to be exposed by a module using the module.exports.

This is an example of a module: orderInquiryController.js:

const getOrderByID = ((req, res) => {
    const orderID = Number(req.params.orderID)
    const order = orders.find(
                  order => order.orderID === orderID)

        if (!order) {
        return res.status(404).send('Order not found')
    }
    res.json(order)
})

const getOrderStatus = ((req, res) => {
    const orderID = Number(req.params.orderID)
    const order = orders.find(
                  order => order.orderID === orderID)

        if (!order) {
        return res.status(404).send('Order not found')
    }
    res.json(order)
})

module.exports = {
                    getOrders,
                    getOrderByID,
                    getOrderStatus
                 }

In this example, we are exporting two functions: getOrderByID and getOrderStatus. Other applications or modules can use these functions by importing the module as explained in the next section.

Importing Blocks of Reusable Code

We can import one or more modules into other modules or applications which want to use the functions defined in those modules.

Let us import the module created in the previous section in another module: orderRoutes.js by using the require function:

const express = require('express')
const router = express.Router()


// Import the orderInquiryController module 
const  { 
    getOrders,getOrderByID,getOrderStatus
} = require('../controllers/orderInquiryController.js')

router.get('/', getOrders)
router.get('/:orderId', getOrderByID)
router.post('/:orderId/status', getOrderStatus)

In this code snippet, we have imported the module: orderInquiryController. We have used a relative path: ../controllers/orderInquiryController.js to specify the location of the module.

We can also publish modules in a shared module registry, and other applications or modules can use them by installing from the shared module registry using the npm package manager. These installed modules reside in the node_modules folder.

Applying the Principle of Separation of Concerns for Organizing Code

Separation of concerns is a principle of software design used to break down an application into independent units with minimal overlap between the functions of the individual units. In Node.js, we can separate our code into different files and directories based on their functionality.

For example, we can keep all our controllers in a controllers directory, and all your routes in a routes directory. This approach makes it easier to locate specific pieces of logic in a huge codebase thereby making the code readable and maintainable.

This is an example of grouping files and folders using the principle of Separation of Concerns by roles:

│   ├── app.js
│       ├── controllers
│       │   ├── inquiryController.js
│       │   └── updateController.js
│       ├── dbaccessors
│       │   └── dataAccessor.js
│       ├── models
│       │   └── order.js
│       ├── routes
│       │   └── routes.js
│       └── services
│           └── inquiryService.js

As we can see, the controller files: inquiryController.js and updateController.js are in one folder: controllers. Similarly, we have created folders for putting other types of files like routes, models, services, and dbaccessors.

This method of grouping by roles should be used for smaller codebases typically in a granular microservice built around 1 feature or domain.

For larger codebases with multiple features or domains, we should organize the code by features rather than by roles as explained in the next section.

Separation of Concerns by Features for Organizing Code

Some Node.js applications could also be composed of multiple features or domains. For example, an e-commerce application could have features: orders, account, inventory, warehouse, etc. Each feature will have a set of APIs which we will build by using a distinct set of controllers and routes.

For these applications, we should organize the code by features to make it more readable.

This is an example of organizing the code of a project by features: accounts and orders.

│   ├── app.js
│   ├── accounts
│   │   ├── controllers
│   │   │   └── accountController.js
│   │   └── routes
│   │       ├── accountRoutes.js
│   │       ├── catalogRoutes.js
│   └── orders
│       ├── controllers
│       │   ├── orderInquiryController.js
│       │   └── orderUpdateController.js
│       ├── dbaccessors
│       │   └── orderDataAccessor.js
│       ├── models
│       │   └── order.js
│       ├── routes
│       │   └── orderRoutes.js
│       └── services
│           └── orderInquiryService.js

Here the files for the features: accounts and orders are placed under folders named: accounts and orders. Under each feature, we have organized the files by the roles like controllers, and routes.

This type of organization makes it easier to locate the code for a particular feature. For example, if we need to check the request handler for the orders API, we can go into the orders folder and look for the controllers kept in that folder.

Using Separate Folders for APIs and Views

The express framework in Node.js allows us to integrate template engines for rendering HTML pages. Whenever we use template engines, it helps to have separate folders for views and APIs:

│    ├── app.js
│         ├── apis
│    │    ├── accounts
│    │    │   ├── controllers
│    │    │   │   └── accountController.js
│    │    │   └── routes
│    │    │       ├── accountRoutes.js
│    │    │       ├── catalogRoutes.js
│    │    └── orders
│    │   ├── controllers
│    │   │   ├── orderInquiryController.js
│    │   │   └── orderUpdateController.js
│    │   ├── dbaccessors
│    │   │   └── orderDataAccessor.js
│    │   ├── models
│    │   │   └── order.js
│    │   ├── routes
│    │   │   └── orderRoutes.js
│    │   └── services
│    │          └── orderInquiryService.js
│         ├── views

Using Separate Folders For Modules of Supported Version of API

Whenever we are supporting multiple versions of APIs we should have separate folders for the modules of each version. In this example, we have two versions: v1 and v2:

│    ├── app.js
│         ├── apis
│    │    ├── accounts
│         │    │    ├──v1
│    │    │    │   ├── controllers
│    │    │    │   │  └── accountController.js
│    │    │    │   └── services
│    │    │    │           └── accountInquiryService.js
│         │    │    └──v2
│    │    │       ├── controllers
│    │    │       │  └── accountController.js
│    │    │       └── services
│    │    │               └── accountInquiryService.js
│    │    └── routes
│         │          └── accountRoutes.js
│    │    └── orders
│    │   ├── controllers

The controller and service modules of version1 are placed under the folder: v1 and the corresponding modules of version2 are placed under the folder: v2.

Placing All Configurations in a Config Folder

Configurations help to prevent hard coding and make it easy to set up the system for different environments. Files with modules containing configurations should be under a folder: config so that it is easy to find and adjust the configuration values in one place.

│    ├── app.js
│         ├── apis
│    │    ├── accounts
│    │    │   ├── controllers
          .    .
          .    .
│    │    └── orders
│    │   ├── controllers
          .
          .
│         ├── config <- Place all config files under this folder
                 ├── dbconfig.test.js
                 └── dbConfig.dev.js

Separate Helpers Folder for Third-party Integration and Common Reusable Code

We always have code that is common to all features for example integration with third-party APIs from Cloud, database connectivity information, utilities like masking information, etc.

These modules should be kept in a separate folder: helpers:

│    ├── app.js
│         ├── apis
│    │    ├── accounts
│    │    │   ├── controllers
│    │    │   │ └── accountController.js 
│    │    │   └── routes
│    │    │     ├── accountRoutes.js
│         │    │          └── catalogRoutes.js 
│    │    └── orders
│    │   ├── controllers
│    │   │   ├── orderInquiryController.js
│    │   │   └── orderUpdateController.js
│    │   ├── dbaccessors
│    │   │   └── orderDataAccessor.js
│    │   ├── models
│    │   │   └── order.js
│    │   ├── routes
│    │   │   └── orderRoutes.js
│    │   └── services
│    │          └── orderInquiryService.js
│         ├── helpers  <- Store code reusable across the project here
│         │       ├── awsServices.js
│         │       └── jwtService.js 

In this example, we have put the modules for connecting to the AWS cloud and utilities for JWT tokens under the helpers folder. If we have too many such files, we can further group them under specialized sub-folders such as integration, authentication, signing, etc.

Separate Folder for Tests for each Feature

Beyond verifying actual and expected results, tests also provide useful information about how the functions exported by the module can be used by the consuming applications. For this reason, test files for modules should be kept under the folder for modules as shown in this example:

│    ├── app.js
│         ├── apis
│    │    ├── accounts
          .
          .
          .
│    │    └── orders
│    │   ├── controllers
          .
          .
          .
│    │   └── orders.spec.js  <- Module specific tests
│         ├── tests  <- Common Tests
│    │    ├── orders
│    │    │   │   └── order_placement.spec.js
│    │    ├── accounts
│    │    │   │   └── account_open.spec.js
│

In this project, the test file for the modules under the orders folder is kept in the same folder. Additional test files are kept in a separate test folder.

Grouping All Shell Scripts in a Separate Folder for Scripts

We often use scripts for configuring the run time environment and dependent systems. Examples of configuration scripts are database initialization scripts, setting up values of environment variables, etc. All such these scripts should be in a separate folder: scripts

│    ├── app.js
│         ├── apis
│    │    ├── accounts
│    │    │   ├── controllers
│    │    │   │       └── accountController.js
│    │    │   ├── routes
          .
          .
          .

│         ├── scripts  <- All the scripts are kept here
│    │ ├── setup_server.js
│    │ └── setup_db.js
│        

In this folder structure, we have stored the scripts for setting up the server: setup_server.js and database: setup_db.js under the folder: scripts.

Enforcing Code Quality with Linters

A linter is a tool that analyzes our code and checks for syntax errors, coding style, and other issues. We should use a linter to maintain consistent quality of code across our entire codebase. Some popular linters for Node.js are ESLint and JSHint.

Periodic Reorganizing of Code

We should revisit the organization of code periodically because the assumptions and demands on the codebase keep changing as an application evolves to fulfill business needs. Some examples of these changes are the introduction of new features requiring the use of a new flavor of a database, and integration with external APIs.

Using a Consistent Naming Convention

Apart from the rules around organizing code, we should also use a consistent naming convention for our files, folders, and functions. Consistent naming helps to increase the readability of our code. We can use a variety of naming conventions, like camelCase, PascalCase, and snake_case. Irrespective of our choice, we should ensure that the naming is consistent across our entire codebase.

Conclusion

Organizing code in a Node.js application is crucial for improving the readability, maintainability, and extendability of our code. Here are the main techniques for code organization:

  1. Modules are the fundamental unit of organizing code in Node.js.
  2. Modules are imported by other applications or modules which need to use the functions defined in the imported modules.
  3. We apply the principle of Separation of Concerns for organizing code.
  4. For small projects like granular microservices built around 1 feature or domain, we should organize by roles like controllers, routes, etc. For bigger projects with multiple features or domains, we should organize by features and then by roles.
  5. Whenever we are supporting multiple versions of APIs, we should have separate folders for the modules of each version.
  6. Files with modules containing configurations should be under a folder: config so that it is easy to find and adjust the configuration values in one place.
  7. Whenever we use template engines, it helps to have separate folders for views and APIs
  8. We should revisit the organization of our code periodically because the assumptions and demands on the codebase keep changing as an application evolves to fulfill business needs.
  9. We should also use a consistent naming convention for our files, folders, and functions. Consistent naming helps to increase the readability of our code.
Written By:

Pratik Das

Written By:

Pratik Das

Software Engineer, Consultant and Architect with current expertise in Enterprise and Cloud Architecture, serverless technologies, Microservices, and Devops.

Recent Posts

Inheritance, Polymorphism, and Encapsulation in Kotlin

In the realm of object-oriented programming (OOP), Kotlin stands out as an expressive language that seamlessly integrates modern features with a concise syntax.

Read more

Publisher-Subscriber Pattern Using AWS SNS and SQS in Spring Boot

In an event-driven architecture where multiple microservices need to communicate with each other, the publisher-subscriber pattern provides an asynchronous communication model to achieve this.

Read more

Optimizing Node.js Application Performance with Caching

Endpoints or APIs that perform complex computations and handle large amounts of data face several performance and responsiveness challenges. This occurs because each request initiates a computation or data retrieval process from scratch, which can take time.

Read more