All Articles
Application modernization

Node.js at scale: tips for building and maintaining large Node.js applications

Georgi Ignatov
22 Mar 2023
15 min read
Georgi Ignatov
22 Mar 2023
15 min read
Node.Js At Scale Tips For Building And Maintaining Large Node.Js Applications Resolute Website

Building large Node.js apps can be challenging as you need to carefully consider the program’s design, scalability, and maintainability. JavaScript is single-threaded, unlike most other multi-threaded web runtimes. If you don’t organize your system correctly, it is easy to hit rock bottom performance using the event loop architecture in the middle of expanding your business. In this article, we’ll review some essential factors to consider while creating big Node.js apps.

The traditional multi-threaded processing model

Each server application in a typical multi-threaded application runtime runs in a process with access to a pool of several but restricted execution threads. The server selects a thread from the pool when it gets a client request. This thread will handle all the processing for that request. The processing within these threads is sequential and synchronous, i.e., one action is done at a time.

Node.Js At Scale Tips For Building And Maintaining Large Node.Js Applications IA 1 WEBNode.Js At Scale Tips For Building And Maintaining Large Node.Js Applications IA 1 Mobile

The single-threaded event loop architecture in Node.js

In contrast to the usual multi-threaded application runtime, Node.js processes all incoming requests in a single thread. This is made feasible by the “event loop,” a continually running computation loop that performs the next in-line task from a queue of computing tasks. These can include incoming HTTP requests, file read/write operations, database queries, outbound network connections, or anything else that necessitates server processing time.

The event loop is the main component that enables Node.js to perform otherwise blocking I/O operations in a non-blocking manner. It continually monitors the progress of your asynchronous tasks and returns them to the execution queue when they are finished. Even though there is only one main thread on the surface, the system kernel has many auxiliary threads that node.js may use for significant disk and network-based async activities. The worker pool is built into libuv and may create and manage numerous threads as needed. These threads can do their respective assigned tasks synchronously and provide their replies to the event loop when they are finished. While these threads work, the event loop can continue processing other requests.

Node.Js At Scale Tips For Building And Maintaining Large Node.Js Applications IA 2 WEBNode.Js At Scale Tips For Building And Maintaining Large Node.Js Applications IA 2 Mobile

When is it advisable not to use Node.js?

When you need to conduct a lot of CPU processing, it is best to avoid Node.js. The event loop in Node.js manages asynchronous or non-blocking actions. The event loop in Node.js uses a single thread; if that thread is halted, the event loop is likewise blocked. As a result, Node.js is not the ideal solution for data processing or demanding CPU operations.

Why does a good project setup matter?

A robust project structure configuration is the solid foundation for a successful application and is essential for any software engineering pipeline. A well-defined structure established ahead of time offers a clear bird's eye view of your system's operation. It also aids in the systematic organization of your business logic, services, API routes, data models, and so on. Structure produces coherence and clarity regarding the role and placement of various components in your project.

Node.Js At Scale Tips For Building And Maintaining Large Node.Js Applications IA 4

Best practices for designing large Node.js applications

In this section, we want to dive deep into the most significant components of designing scalable Node.js applications.

1. Layered architecture approach

Node.Js At Scale Tips For Building And Maintaining Large Node.Js Applications IA 3 WEBNode.Js At Scale Tips For Building And Maintaining Large Node.Js Applications IA 3 Mobile

According to the "separation of concerns" principle, we should have various modules for tackling different problems relevant to our application. Regarding server-side apps, separate modules (or layers) should cater to various parts of processing a response to a client request. In most cases, it will look like this: a client request + some business logic + some data modification + response. These considerations can be addressed by programming three distinct levels, as indicated below.

  • Controller layer (API routes and endpoints)
  • Service layer (for business logic)
  • Data access layer (for working with a database)
Node.Js At Scale Tips For Building And Maintaining Large Node.Js Applications IA 4 WEBNode.Js At Scale Tips For Building And Maintaining Large Node.Js Applications IA 4 Mobile

1.1 Controller layer

The API routes are defined in this module of your code.

Only your API routes are defined here.

You can break down the request object in the route handler functions, select the key pieces of information, and send them to the service layer for processing.

1.2 Service layer

Your application’s business logic and even its secret sauce reside here.
It has numerous reusable classes and methods that focus on a particular purpose (along with other SOLID programming concepts).

Using this layer, you can effectively decouple the processing logic from the location where the routes are defined.

Another element to consider here is the database component.

We need another layer so that we can handle this independently.
The Service Layer MUST:

  • Include business logic
  • To interface with the database, use the data access layer.
  • Be framework independent.

The Service Layer CANNOT:

  • Accept req or res objects.
  • Handle client responses
  • Provide anything relating to the HTTP Transport layer, such as status codes, headers, and so on.
  • Interact with the database directly

1.3 Data access layer

The task of talking to the database and reading from, writing to, and updating it can be handled by the data access layer.

You should define all your SQL queries, database connections, models, object-relational mappers, etc. here.
Most Node.js applications can be built on this sturdy foundation, making it easy to design, manage, debug, and test your applications.

// api
//  |--students_controller.js

const StudentsService = require('../services/students_service');

async function studentsIndex(req, res){
  const page = req.query.page;
  const pageSize = req.query.pageSize;
  const filters = {};
  try{
  	const list = await StudentsService.paginate({ filters, page, pageSize });
    return res.json({
      success: true,
      data: {
        students: list,
        page,
        pageSize,
        filters
      }
    });
  }catch(e){
    return res.status(500).json({
      success: false,
      data: {
        error: e.message
      }
    });
  }
}

async function studentsCreate(req, res){
  const data = req.body;
  const validation = await StudentsService.validate(data);
  if(validation.errors.length > 0){
    return res.status(422).json({
      success: false,
      data: {
        errors: validation.errors
      }
    });
  }
  const student = await StudentsService.create(data);
  return res.json({
    success: true,
    data: {
      id: student
    }
  })
}

async function studentsUpdate(req, res){
  const data = req.body;
  const studentId = req.params.id;
  const validation = await StudentsService.validate(data);
  if(validation.errors.length > 0){
    return res.status(422).json({
      success: false,
      data: {
        errors: validation.errors
      }
    });
  }
  const student = await StudentsService.update(studentId, data);
  return res.json({
    success: true,
    data: {
      id: student
    }
  })
}

async function studentsShow(req, res){
  const studentId = req.params.id;
  const student = await StudentsService.get(studentId);
  if(!student){
    return res.status(404).json({
      success: false,
      data: {
        errors: ['Student with id '+ id +' not found']
      }
    });
  }
  return res.json({
    success: true,
    data: student
  })
}

async function studentsDelete(req, res){
   	const studentId = req.params.id;
  	try{
   		const student = await StudentsService.delete(studentId);
      return res.json({
        success: true,
        data: student
      });
    }catch(e){
      return res.status(404).json({
        success: false,
        data: {
          errors: ['Student with id '+ id +' not found']
        }
      })
    }
}

In the example above, we see a controller from the layered approach. The controller has a bare minimum of business logic, only mapping data to the service layer from the request and acting on the result returned from the service layer, which under the hood, interacts with the data access layer and handles all the necessary business logic. The data access layer interacts with the database, so the controller remains clean and free of unnecessary code.

2. Use Typescript

We have seen some critical bugs reported during the application runtime that could have been easily prevented; the bug involved calling a function with the wrong parameters. When you switch to TypeScript from plain JavaScript, this problem is eliminated. Although rigorous unit tests can solve it, you can never guarantee 100% test coverage. It can turn out to be a million-dollar mistake.

3. Style guides

In addition to formatting and linting, you can also look at the JavaScript coding standards followed by industry titans like Google and Airbnb. These guides cover a wide range of topics, including formatting specifics, file encodings, naming conventions (for filenames, variables, classes, etc.), and much more. They can help you produce high-quality code that adheres to the standards and methods of some of the world’s best developers.

4. Use environment variables

As your program grows, you’ll realize that some global configuration options and settings must be accessible across all modules.
It is always a good idea to group these options in a single file within a project’s config folder. The dotenv package is a popular Node.js library for this.

5. Use a process manager

Process management makes it simple to orchestrate many Node.js processes and keep them always functioning. If your program dies because of a memory leak or unhandled exception, the process manager restarts it and guarantees it continues functioning. If an unhandled exception occurs in your code, the process management will catch it and restart the program to ensure no downtime. Some of the most popular process managers include:

  • Forever is a simple tool for ensuring that a Node.js script runs continuously.
  • PM2 - Daemon process manager that will help you manage and keep your application online 24/7.
  • Nodemon: simple; not recommended for anything outside of the development environment.
  • Supervisor is a process control system for UNIX-based systems with support for Node.js applications.
  • Systemd is the system and service manager for Linux systems, with support for managing Node.js applications.

For simplicity, we will only focus on PM2 in this article, but feel free to check out the rest and find the one most suitable for your needs.

What PM2 can offer you:

  • PM2 can automatically start your Node.js processes when the server restarts, ensuring your application is always accessible.
  • PM2 supports simple load balancing by distributing incoming requests across multiple Node.js processes.
  • PM2 offers real-time performance monitoring and analysis, allowing you to rapidly discover and resolve performance issues.
  • PM2 offers cluster mode, which allows you to effortlessly expand your Node.js application by running several processes simultaneously.
  • PM2 is a popular choice for Node.js process management since it is simple to set up and use.

6. Use a framework

Frameworks drastically reduce the amount of time and effort necessary to create software. They give you a set of tools to construct your application without reinventing the wheel by increasing the code. The essential information is already available. As a result, the developer may spend more time designing code specific to the project and less time managing the framework’s tiresome, repetitive tasks. They make the code clear and adaptable by conforming to the framework’s coding style. Frameworks also increase application development productivity, which reduces the cost of building a feature.

On the other hand, if you don’t know what you’re doing or where you’re headed, the framework might be a double-edged sword.
Using a framework may present you with the following challenges:

  • Frameworks are a one-size-fits-all solution with some limitations. Because a single framework cannot accomplish everything, some may not be suitable for specific applications.
  • They are unsuitable for small projects since setting up the framework takes longer than bespoke coding.
  • There is a steep learning curve for new users of the framework. Before becoming productive, the developer must first learn how to utilize it appropriately.
  • Any defects or security flaws in a framework can harm the application developed on it.

Below we compiled a list of the best frameworks for Node.js:

Express

This is the most well-known framework for Node.js developers. It provides a complete environment to carry out your requirements, with capabilities organized into numerous modules and an incredibly smooth learning curve. It is used for a variety of purposes, including:

  1. Developing REST APIs that are used to communicate with client applications
  2. Traditional MVC application development

Nest

An enterprise-level framework inspired by Angular and employs Typescript. It is built modularly and intended for contemporary development, with business-grade structures and clean code. Furthermore, it has a sizable and quickly growing community. You can use it for:

  1. Developing REST APIs that are used to communicate with a client application
  2. Traditional MVC application development
  3. Working with hexagonal architecture or other complex architectures like CQRS
  4. Supports microservice architecture out of the box.

Adonis

It is perfect for Django, Rails, and Laravel developers. It offers a wide range of features, allowing you to focus solely on project development while saving time by eliminating the installation of particular libraries. It is Laravel for Node.js, has many batteries included, and is intended for rapid application development. Use it for:

  1. Developing REST APIs that are used to communicate with a client application
  2. Traditional MVC application development

Final words

In conclusion, Node.js is a highly efficient platform for developing applications. Its speed, scalability, and versatility make it a great option for businesses of various sizes. If you’re considering creating a custom Node.js app, our team of skilled professionals can assist you in bringing your ideas to life.

We invite you to reach out to discuss your project. Let’s work together to make your Node.js app a success!

Node.js
Mobile Apps

stay tuned

Subscribe to our insights

Secured with ReCAPTCHA. Privacy Policy and Terms of Service.