Node.js Best Practices

Node.js is winning every battle of web development by offering faster performance, improved security, ease of development, and a smaller learning curve since its inception.

It's easy to get started on Node.js projects, but once we are beyond the basics knowing how to structure code, handling errors, code styling and the right production practices will eventually make the difference.

In this article, I will be listing down a few best Node.js practices that will improve you as a developer and make a rock-solid production application.

1. Project Structure

  • Use Self-Contained Component as opposed to grouping by technical role. Develop small software with minimal dependencies and make way for microservice architecture and avoid a monolith that is hard to manage.

component-basis

Self-Contained Component

Technical Role

Technical Role

  • Avoid passing the request object provided by express in your data layer. As the used function becomes dependent on express thus making it unavailable in testing codes, CRON jobs, and other non-express callers.

  • Maintain a separate file for your routes and their function.

  • Wrap common packages and publish it as a private package at npm, and sharing it across multiple codebases.

  • Maintain a hierarchy of variables in the env file for better readability.

2. Error Handling

  • Use  Async/Await, promises over callback as it provides a more compact syntax and is easy to maintain Moreover, callback deprives us of return, throw, and the stack trace. The Promise method is much more compact, clearer and quicker to write.

  • Use the node built-in error object(throw new Error("Error message")) rather than just returning a string, through the error object the “stack trace” details the point in the code at which the Error was instantiated, and a text description of the error.

  • Use logging tools to increase errors visibility(Winston, Bunyan, Log4js). We can query the error logs thus speeding up our development process.

  • Using Testing Frameworks like Mocha & Chai to make way for TDD

  • Use the unhandledRejection event defined using:

    process.on('unhandledRejection', error => {
     console.log('unhandledRejection', error.message);
    });
    
    
    This will ensure that any promise error, if not handled locally, will get caught over here

3. Code Style

  • Use ESLint, prettier for checking possible code errors, and fixing code style.

  • Opening curly braces of a code block should be on the same line as the opening statement.

    Below is one of the pitfalls of JavaScript: automatic semicolon insertion. The below

    function will return undefined.
    function test()
    {
     return; //notice the inserted semicolon
     {
       javascript: "fantastic"
     };
    }
    

  • Avoid using anonymous functions as they won't be available in memory snapshot.

  • Follow the following order if you start a project in JS: const(first), let if you want to reassign values and var never.

  • Require modules at the beginning of each file for more readability. Moreover, require statements are synchronous so if they are called from within a function, it may block other requests from being handled at a more critical time.

  • Use arrow function expressions (=>) which make the code structure more compact, take care of this binding.

4. Testing

  • Implementing Test Driven Development in your SDLC. Start with API testing which provides more coverage than unit testing. Then continue with advanced test types like unit testing, DB testing, performance testing, etc

  • Structure your test case well so it's self-explanatory to QA engineers and developers who are not familiar with the code internals.

    • State in the test name and what is being tested.

    • Under what circumstances and what is the expected result.

    • Avoid global tests and each test acts on its own set of data.

    • Avoid mutating of DB record.

5. Production

  • Monitor your app. By defining certain metrics like CPU, server RAM, Node process RAM, the number of errors in the last minute, number of process restarts, average response time to ensure a healthy state of the application

  • Node is bad at doing CPU intensive tasks like gzipping, SSL termination, etc. Use middleware services like nginx, HAproxy for these tasks.

  • Using express for (networking related tasks) like serving static files, gzip encoding, throttling requests, SSL termination, etc is a performance kill due to its single-threaded model which will keep the CPU busy for long periods, Nginx can do a much better job of handling static files and can prevent requests for non-dynamic content from clogging our node processes.

  • Use a process manager(like pm2) to ensure automatic restarts, utilize all CPU cores by implementing clustering.

  • Lock dependencies of npm packages ensuring that the CI environment and QA will test software with exactly the same package version that will later be sent to production.

  • Store any type of data (e.g. user sessions, cache, uploaded files) within external data stores, enforce a stateless behavior.

6. Security

  • Implement rate-limiting (using express-rate-limit, rate-limiter-flexible) to help protect against DDoS attacks by limiting the incoming request rate to a value typical for real users.

  • Support blacklisting of JWTs to stop malicious activity once detected.

  • Prevent query injection vulnerabilities with ORM/ODM libraries (e.g. Sequelize, Knex, mongoose)

  • Avoid publishing secrets to your code hosting platforms.

  • Use Helmet to secure your Express apps by setting various HTTP headers.

Wrapping up

So, these were some of the practices you can start using in your application and improve as a developer.

© 2021 Asim Ansari | Software Engineer at Simpplr