Beginners Guide On How To Build A Secure, Scalable And Efficient Nodejs App

Introduction

Nodejs is an open-source, cross-platform runtime environment for JavaScript and it is a well-liked tool for practically every kind of project!

The core of Google Chrome, the V8 JavaScript engine, is run by Node.js outside of the browser. This allows Node.js to be highly performant.

Node.js app runs in a single process, without creating a new thread for every request. Instead of pausing the thread and spending CPU time waiting for an I/O action, such as reading from the network or accessing a database or disk, Node.js will continue its work once the response has been received.

This eliminates the complexity of managing thread concurrency, which might be a substantial source of errors, and allows Node.js to manage thousands of concurrent connections with a single server.

Below, We'll list how the guides on how to effectively build a highly scalable, efficient and secure Nodejs app

Properly Structure your folder

This isn't peculiar to the node js app alone but to all software applications that's been developed.

The ability to follow a proper structure makes it easier to code properly, debug effectively

The root of a system should contain folders or repositories that represent reasonably sized business modules. Each component represents a product domain like 'user-component', 'order-component', etc. Each component has its API, logic, and logical database

The mental load, development friction, and deployment dread are considerably lower and better with autonomous components because every modification is carried out over a finer and smaller scope. Developers can move considerably more quickly as a result.

Use TypeScript

Coding without type safety is no longer an option and typescript is the most common static typing programming language. you can use it to define variables and function return types.

Typescript is javascript with added syntax for

Typescript can highlight unexpected behaviors in code and help detect lots of bugs before they're released to production

Use Async-Await for async error handling

Async-await is the simplest way to express an asynchronous flow as it makes asynchronous code look synchronous

The use of Promises with async-await, which offers a much more condensed and well-known code syntax like try-catch, is the nicest present you can give your code.

The promises method is a lot shorter, more concise, and easier to write. Any time an error or exception happens within one of the ops, the single.catch() handler takes care of it. You can manage all errors in one place, eliminating the requirement for error checking at each stage of the process.

Don't handle an error within a middleware

To avoid code duplication and improperly handled errors, always handle your error in a centralized object.

All entry points (such as APIs, cron jobs, and scheduled processes) should call the dedicated and centralized object that contains the error-handling logic when an error occurs. This object should have code for logging, determining whether to crash and monitoring metrics.

The likelihood of inconsistent error handling increases in the absence of a single dedicated object: Errors that are raised during web requests may be handled differently from startup errors and scheduled job errors. This could result in some forms of poorly managed errors.

Use a logger to increase errors visibility

Encountering error is a normal cycle when building and testing nodejs application and making it easy to view and read such errors are key to debugging.

My go-to logger is Pino. Pino is a powerful logging framework for Node.js that boasts exceptional speed and comprehensive features.

The ease of integration of Pino with other Node.js web frameworks adds to its adaptability, making it a top pick for developers searching for a dependable and adaptable logging solution.

All the typical capabilities present in any logging framework, such as programmable log levels, formatting choices, and various log conveyance modes, are present in Pino. One of its distinguishing qualities is its flexibility, which may be quickly increased to match certain requirements, making it an excellent choice for a variety of applications.

Identify Errors And Monitor Downtime With APM Products

When building in production or after being deployed to production, you need to be able to identify errors and monitor downtimes automatically. APM (Application Performance Management) products help in monitoring and management of the performance and availability of software applications. To maintain the level of service that is required, APM works to identify and treat complicated application performance issues.

Traditional error handling presupposes the existence of an exception as an issue with the code, however, application problems can also be caused by slow code paths, API outages, a lack of computational resources, and other factors. This is where APM technologies can help, as they enable proactive detection of a wide range of "buried" issues with a minimal setup.

I'll share my go-to APM products based on the three 3 major segments

1.) Website or API monitoring ( UpTimeRobot.Com )

2.) Code instrumentation (AppDynamics.Com)

3.) Operational intelligence dashboard (datadoghq.com)

This might be overkill when working with low/mid nodejs applications but it might come in handy when needed.

Validate Your Input

User input validation is one of the challenges of creating backend APIs. You should always include an additional layer of validation to incoming data since you can never rely just on user input.

We need to validate our inputs because client-side validation is really not enough as it can easily be subverted, inputs can easily be manipulated and more prone to Man in the middle attack

Data validation libraries are among the most useful in the Node.js ecosystem and there are tons of them out there.

Node data validation library is modules that aid developers in guaranteeing the accuracy and integrity of the data in their applications. These libraries offer tools and routines that enable applications to manage errors, validate data, and perform data checks.

My preferred data validation library is Zod. It's very lightweight, multiplatform, supports many languages and is easy to configure.

Joi is also a very good validation library but it doesn't work with Typescript

Don't use Var, Use Const and Let Instead

var are either globally or functionally scoped, they do not support block-level scope. This means that if a variable is defined in a loop or in an if statement it can be accessed outside the block and accidentally redefined leading to a buggy program

Let variables declared is only available in the block scope in which it was defined. Let and var both allow you to reassign the value at a later time, making them quite similar to each other. Let deals with a block scope, and while it can be reassigned, it cannot be redeclared, which is the primary distinction between the two.

const as its name sound means that once a variable is assigned, it cannot be reassigned. It's constant to the variable and it cannot only be redeclared but also cannot be reassigned.

Const declarations for variables are best practice; if you later find that the value of the variable needs to change, go back and make the necessary changes to let

Use the === operator

Many newbie developers make the mistake of thinking === and == are the same which is wrong

=== is a strict equality operator as against == which is a weaker equality operator.

There is no type of conversion in === and both variables must be of the same type to be equal as against == which might return true for unequal variables.

Below, I'll share the differences and similarities between === and ==

0 == 0; // true           ||  0 === 0; // true
"" == "0"; // false       ||  "" === "0"; // false
0 == ""; // true          ||  0 === "";  // false
0 == "0"; // true         ||  0 === "0"; // false

true == "true"; //true   || true === "true"; // false
true == true; //true     || true === true; // true

null == undefined; // true || null === undefined; // false

Avoid Code With effects outside of functions

Avoid placing code outside of routines that have effects, such as network or database calls. When another file needs the file, this code will be performed as soon as possible. When the underlying system is not yet prepared, this "floating" code might be executed which might lead to performance issues.

Error handlers, environment variables, and monitoring are often set by a web framework. Before the web framework is initialized, DB/network calls won't be monitored or fail as a result of a lack of configuration information.

Put your DB/network code inside of functions that must be explicitly called. Consider using the factory or disclosing module patterns if some DB/network code needs to be executed immediately after the module loads.

Test your Code

When building for production, testing is a must and should be part of your developmental cycle. Whether automatic or/and manual testing, testing is paramount.

We understand you might be in a rush to meet up with a deadline and push to production. At the very least, do an integration testing.

The fundamental goal of integration testing is to evaluate a component as a whole in its current state, using the API and all applicable layers, including the database. This increases developer experience and confidence.

You don't want to break production by pushing code that does not interact with another component.

My go-to tools for testing are Jest for code testing and Postman for API

Write Clean Code

The iterative development flow includes the critical process of refactoring. Your code will be improved and become easier to maintain if you eliminate poor coding practices like duplicate code, lengthy methods, and long parameter lists.

We can write cleaner codes using Linting tools and static analysis tools

Most linting tools will focus on code styles like indentation and missing semicolons in a single file while static analysis tools focus on finding duplicate code, complexity analysis etc that are in single files and multiple files.

You can use ESLint and Prettier as your linting tools which helps in detecting potential code errors and fixing code style. It helps in identifying nitty-gritty spacing issues and also detect serious code anti-patterns.

SonarQube is a popular static code analysis tool that can be used for security testing and error detection. It is an open-source program that allows for automatic reviews and ongoing code quality checks in both free and premium versions.

Poor code quality will always result in bugs and poor performance, which can never be fixed by a brand-new library or cutting-edge features.

Delegate Task

Due to Nodejs single threaded model which will keep the CPU busy for long periods, there are some flaws and vulnerabilities in Node.js that might lead to subpar performance or even crashes in Node-based apps. For instance, Node. js-based web applications are vulnerable to IO-bound activities or rapid traffic growth, which can cause delayed code execution or even crashes. Additionally, they can have issues when distributing static material across many servers, such as photos and JavaScript files.

To avoid this issue, it's better to delegate certain tasks such as serving static files, gzip encoding, throttling requests, SSL termination, etc. while working on a large-scale node js application

A better approach is to use a tool that expertise in networking tasks –the most popular and highly recommended is Nginx which can be used to lighten the incoming load on node.js processes

Be Stateless

Store any type of temporary/frequently used data such as user sessions, cache e.t.c within external data stores

In a stateless application, The server doesn't keep any records of the user's session information. Instead, all the information required for authentication is included in each request the user makes to the server, often in the form of a JWT. Following that, the server verifies the token and responds appropriately.

Because stateful authentication requires the server to retain the state, which can become problematic with big user bases, stateless authentication is quite popular among current web applications.

Always use an ORM/ODM

When interacting with databases, two popular techniques used in interacting with databases are object-relational mapping (ORM) and object-document mapping (ODM). Both ORM and ODM offer an abstraction layer that enables programmers to interact with databases using the objects of their chosen programming language rather than by writing raw SQL queries. This makes writing, maintaining, and testing the code simpler.

Using an ORM/ODM protects you from database injection vulnerabilities as they ensure the usage of parameterized queries and data bindings to guarantee the validated data is handled properly and avoided creating unnecessary attack vectors.

ORM is used to interact with relational databases such as MySQL, PostgreSQL, and Oracle which store information in tables, each of which represents a particular entity and each of whose rows represents an instance of that entity. Using foreign keys, the relationships between tables are described.

ODM is used to interact with document-based databases, such as MongoDB. Data is stored in document-based databases as documents, with each document serving as an instance of an entity. Compared to relational databases, these databases are more adaptable because the document structure can differ from one document to another.

To stop attackers from exploiting widespread techniques like cross-site scripting (XSS), clickjacking, and other malicious attacks, your application should use secure headers.

It's always a good idea to utilize the most recent HTTP security headers because they can be a simple approach to increasing online security and frequently don't require changes to the application itself. However, it can be challenging to stay on top of things when working with hundreds of websites because browser vendor support for HTTP headers can change so frequently.

There are many tools and modules to help with configuration of your headers but the most popular and recommended is the helmet module for express

Conclusion

Node.js is a powerful platform that can be used to build a wide variety of applications. However, in order to create Node.js apps that are secure, scalable, and effective, it is crucial to adhere to best practices.

Here are some salient takeaways from this guide:

  • Test your app thoroughly to identify and fix any security or performance issues.

  • Improve the performance of your app using caching and other methods.

  • use a secure framework or library.

  • use a scalable database o manage vast amounts of data

  • To enhance performance, use asynchronous programming approaches

  • To make sure that your program is using the most recent versions of its dependencies, dependency management is crucial.

  • Automate the build, test, and deploy procedure using a continuous integration and continuous delivery (CI/CD) pipeline

  • Keep an eye on your app's performance to spot any problems and resolve them

By adhering to these best practices, You can create Node.js programs that are secure, scalable and effective .

0
Subscribe to my newsletter

Read articles from Balogun Abdulquadri directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Balogun Abdulquadri
Balogun Abdulquadri

I am a fullstack software and blockchain developer building test driven enterprise graded application