The Node.js Best Practices List for 2024
Welcome to the future of server-side JavaScript! As we step into 2024, Node.js continues to be an indispensable part of the web development ecosystem. With its non-blocking I/O model and the massive npm ecosystem, Node.js has evolved, bringing new best practices that developers should follow to ensure efficient, secure, and maintainable applications. Here’s your guide to the Node.js best practices for 2024.
1. Project Architecture Practices
✔️ 1.1 Structure Your Solution by Business Components
In a Nutshell: Organize your system into business-centric modules. Each module, or “component,” like ‘orders’ or ‘users,’ encapsulates its API, logic, and database. This granular approach speeds up development by reducing complexity.
my-system/
├─ apps/
│ ├─ orders/
│ ├─ users/
│ ├─ payments/
├─ libraries/
│ ├─ logger/
│ ├─ authenticator/
The Risk of Ignoring: Mixing different business logic can lead to a tangled system where changes have unpredictable consequences, slowing down development and increasing deployment risks.
✔️ 1.2 Layer Your Components with 3-Tiers
In a Nutshell: Adopt the 3-Tier architecture within each component for clarity: an ‘entry-point’ for controllers, ‘domain’ for business logic, and ‘data-access’ for database interactions.
my-system/
├─ apps/
│ ├─ component-a/
│ │ ├─ entry-points/
│ │ │ ├─ api/ # Controllers reside here
│ │ │ ├─ message-queue/ # Message consumers reside here
│ │ ├─ domain/ # Business logic
│ │ ├─ data-access/ # Direct DB access without ORM
The Risk of Ignoring: Allowing web layer objects like requests/responses into the domain layer breaks the separation of concerns and hampers future scalability.
✔️ 1.3 Wrap Common Utilities as Packages
In a Nutshell: Isolate reusable code in a “libraries” directory. Create a package.json
for each to manage dependencies and potentially prepare for publishing.
my-system/
├─ apps/
│ ├─ component-a/
├─ libraries/
│ ├─ logger/
│ │ ├─ package.json
│ │ ├─ src/
│ │ │ ├─ index.js
The Risk of Ignoring: Without proper encapsulation, internal module functions can become entangled with external dependencies, leading to a brittle codebase.
✔️ 1.4 Use Environment Aware, Secure, and Hierarchical Config
In a Nutshell: Configuration management should allow for both file and environment variable sourcing, exclude secrets from the codebase, provide hierarchical structure, support typing, include validation, and define defaults.
The Risk of Ignoring: Missing out on a critical environment variable could lead to partial system failures and inconsistent states, making debugging a nightmare.
✔️ 1.5 Consider All Consequences When Choosing the Main Framework
In a Nutshell: The choice of framework—whether it’s Nest.js, Fastify, Express, or Koa—should be made after thorough consideration of your project’s scale, complexity, and preferred programming paradigm. Read our full framework comparison guide.
The Risk of Ignoring: A hasty decision can lead to a mismatch between project requirements and framework capabilities, potentially hindering future growth and development.
✔️ 1.6 Use TypeScript Sparingly and Thoughtfully
In a Nutshell: While TypeScript’s type safety is indispensable, its sophisticated features can introduce unnecessary complexity. Use advanced TypeScript features judiciously, and stick to simple types for most of your codebase.
The Risk of Ignoring: Over-reliance on complex TypeScript features can obscure code logic and increase the learning curve, which may lead to more bugs and longer debugging sessions.
2. Error Handling Practices
✔️ 2.1 Use Async-Await or Promises for Async Error Handling
TL;DR: Avoid the notorious callback hell by embracing Promises and async-await patterns. They allow you to handle asynchronous operations with a synchronous flow, making your code cleaner and more readable with traditional try-catch blocks.
The Risk of Ignoring: Sticking to the outdated callback pattern can lead to deeply nested code that’s hard to read, maintain, and debug—a direct route to an unmaintainable codebase.
✔️ 2.2 Extend the Built-in Error Object
TL;DR: Standardize your error handling by extending the built-in Error object. This ensures all errors thrown or rejected in your application have a consistent structure, complete with properties like name
, code
, and isCatastrophic
.
class ApplicationError extends Error {
constructor(message, code) {
super(message);
this.name = "ApplicationError";
this.code = code;
this.isCatastrophic = false;
}
}
The Risk of Ignoring: Without a unified error structure, you may encounter a mix of string-based and custom errors that complicate handling logic and potentially lose valuable debugging information like stack traces.
✔️ 2.3 Distinguish Catastrophic Errors from Operational Errors
TL;DR: Operational errors, like invalid user input, are expected and can be handled gracefully. Catastrophic errors, such as programming mistakes, often require a more drastic response, like restarting the application.
The Risk of Ignoring: Treating all errors as equal can lead to overreacting to minor issues or underreacting to critical ones, neither of which is conducive to a stable application environment.
✔️ 2.4 Handle Errors Centrally, Not Within a Middleware
TL;DR: Centralize your error handling logic. Create a dedicated error handler that is responsible for logging, deciding on application crashes, and monitoring, ensuring consistency across various parts of your application.
The Risk of Ignoring: Scattered error handling leads to repetition and inconsistencies, making it difficult to ensure errors are handled properly everywhere.
✔️ 2.5 Document API Errors Using OpenAPI or GraphQL
TL;DR: Clearly document the potential errors your API may return using standards like OpenAPI for REST APIs or the schema and comments in GraphQL. This helps consumers handle errors correctly and avoid unnecessary crashes.
The Risk of Ignoring: Clients that are unaware of possible errors may implement poor error handling, leading to crashes and a subpar user experience.
✔️ 2.6 Exit the Process Gracefully When a Stranger Comes to Town
TL;DR: On encountering an unknown error, make it observable, then gracefully shut down to maintain a predictable application state. Modern orchestration tools like Kubernetes or serverless platforms will handle the restart.
The Risk of Ignoring: Ignoring unknown errors can leave your application in an undefined state, which might lead to unpredictable behavior and data corruption.
✔️ 2.7 Use a Mature Logger to Increase Errors Visibility
TL;DR: Adopt a robust logging tool like Pino or Winston to give visibility to your errors with features like log levels and pretty printing. Direct logs to stdout
and let your infrastructure handle the rest.
The Risk of Ignoring: Relying on console.log
for error logging is insufficient for production applications, making it challenging to track and respond to issues efficiently.
✔️ 2.8 Test Error Flows Using Your Favorite Test Framework
TL;DR: Ensure your testing covers both positive scenarios and error cases. Use your preferred testing framework to simulate errors and verify that your application responds as expected.
The Risk of Ignoring: Unchecked error flows can lead to unexpected application behavior and poor fault tolerance, undermining user confidence.
✔️ 2.9 Discover Errors and Downtime Using APM Products
TL;DR: Utilize Application Performance Monitoring (APM) tools to proactively monitor your application. They can automatically highlight errors, crashes, and performance bottlenecks.
The Risk of Ignoring: Without APM tools, you might miss critical issues that affect user experience, leading to lost customers and revenue.
✔️ 2.10 Catch Unhandled Promise Rejections
TL;DR: Listen for the process.unhandledRejection
event to catch and handle exceptions from promises that were not caught by a .catch()
handler.
The Risk of Ignoring: Unhandled promise rejections can lead to leaks, unobserved state changes, and other unpredictable behaviors.
✔️ 2.11 Fail Fast, Validate Arguments Using a Dedicated Library
TL;DR: Use schema validation libraries like ajv, zod, or typebox to assert API input and prevent bugs that are harder to trace later in the execution flow.
The Risk of Ignoring: Skipping validation can lead to subtle bugs that pass silently through your logic and manifest as bigger issues downstream.
✔️ 2.12 Always Await Promises Before Returning to Avoid a Partial Stacktrace
TL;DR: Always return await
when returning a promise within an async
function to ensure the full stack trace is preserved in case of an error.
The Risk of Ignoring: Omitting await
can result in incomplete stack traces, making it harder to track down the source of an error.
✔️ 2.13 Subscribe to Event Emitters and Streams ‘error’ Event
TL;DR: Subscribe to the ‘error’ event of Event Emitters and Streams to handle exceptions in the context of the event-driven code. For EventTargets, ensure to handle errors contextually since they don’t emit ‘error’ events.
The Risk of Ignoring: Failing to handle errors in event-driven code can lead to uncaught exceptions and potentially crash your Node.js application or leave it in an unstable state.
3. Code Patterns And Style Practices
✔️ 3.1 Use ESLint
TL;DR: Leverage ESLint for its powerful capability to catch errors and enforce code style. While ESLint can automatically fix styles, pairing it with Prettier ensures that your code not only adheres to best practices but also looks consistent.
The Risk of Ignoring: Neglecting automated linting tools can lead to time-consuming discussions about code style and preventable bugs in your codebase.
✔️ 3.2 Use Node.js ESLint Extension Plugins
TL;DR: Complement ESLint with Node.js-specific plugins such as eslint-plugin-node, eslint-plugin-mocha, and eslint-plugin-security to catch Node.js-specific issues and security vulnerabilities.
The Risk of Ignoring: Without Node.js-specific linting rules, you might miss critical security and performance issues unique to the Node.js runtime environment.
✔️ 3.3 Start a Codeblock’s Curly Braces on the Same Line
TL;DR: Maintain readability and prevent errors by starting curly braces on the same line as their corresponding statements.
// Do
function someFunction() {
// code block
}
// Avoid
function someFunction()
{
// code block
}
The Risk of Ignoring: Inconsistent brace placement can lead to errors and confusion, as highlighted in numerous community discussions.
✔️ 3.4 Separate Your Statements Properly
TL;DR: Whether you use semicolons or not, understanding the pitfalls of automatic semicolon insertion is crucial. Use tools like ESLint and Prettier to enforce proper statement separation.
The Risk of Ignoring: Improperly separated statements can lead to syntax errors and unexpected behavior, which can be difficult to debug.
✔️ 3.5 Name Your Functions
TL;DR: Always name your functions, including closures and callbacks. This practice is invaluable for profiling and debugging, especially in production environments.
The Risk of Ignoring: Anonymous functions can make debugging a nightmare, as they hinder the ability to trace the source of errors and performance issues.
✔️ 3.6 Use Naming Conventions for Variables, Constants, Functions, and Classes
TL;DR: Adhere to naming conventions: lowerCamelCase
for variables and functions, UpperCamelCase
for classes, and UPPER_SNAKE_CASE
for global or static constants.
// Global constants
const GLOBAL_CONSTANT = "immutable value";
// Class name
class SomeClassExample {}
// Function name
function doSomething() {}
The Risk of Ignoring: Inconsistent naming can lead to confusion about the nature of variables and functions, and may result in errors when using or instantiating classes.
✔️ 3.7 Prefer const over let. Ditch the var
TL;DR: Use const
to declare variables that should not be reassigned after their initial assignment. For variables that may change, use let
. Avoid var
to prevent function-scoped variables and hoisting issues.
The Risk of Ignoring: Using var
can result in unexpected behavior due to hoisting and function-scoped peculiarities, potentially leading to bugs.
✔️ 3.8 Require Modules First, Not Inside Functions
TL;DR: Always require
modules at the top of your files, not within functions, to avoid synchronous file system operations during runtime and to make dependencies clear.
The Risk of Ignoring: Dynamic requires can lead to performance bottlenecks and obscure the module’s dependencies, making the code harder to read and maintain.
✔️ 3.9 Set an Explicit Entry Point to a Module/Folder
TL;DR: Define an explicit entry point for your modules or libraries to encapsulate the internal structure and present a clear public API.
// index.js of the module
module.exports = {
SMSWithMedia: require('./media-provider')
};
// Consumer code
const { SMSWithMedia } = require('sms-provider');
The Risk of Ignoring: Exposing the internal structure of your modules can lead to tight coupling with consumer code, making future refactoring more difficult.
✔️ 3.10 Use the ===
Operator
TL;DR: Always use the strict equality operator ===
to avoid type coercion and ensure that comparison results are predictable and explicit.
The Risk of Ignoring: Loose equality checks with ==
can result in unexpected type conversions and lead to subtle bugs.
✔️ 3.11 Use Async Await, Avoid Callbacks
TL;DR: Embrace the async-await syntax for its cleaner and more intuitive handling of asynchronous operations over the older callback pattern.
The Risk of Ignoring: Persisting with callbacks can lead to nested and less maintainable code structures, commonly referred to as “callback hell.”
✔️ 3.12 Use Arrow Function Expressions (=>
)
TL;DR: Utilize arrow functions for their concise syntax and lexical this
binding, which can make your code more readable and less prone to common mistakes with this
.
The Risk of Ignoring: Traditional function expressions can lead to verbose code and issues with this
context, which can cause bugs, particularly in callbacks.
✔️ 3.13 Avoid Effects Outside of Functions
TL;DR: Refrain from writing code with side effects, like network or database calls, outside of functions. This ensures that such operations are only performed when explicitly invoked.
The Risk of Ignoring: Code outside of functions can execute at unexpected times, lead to performance issues, and make testing and mocking more challenging.
4. Testing And Overall Quality Practices
We have comprehensive guides dedicated to testing. The list here is a condensed summary:
✔️ 4.1 At the Very Least, Write API (Component) Testing
TL;DR: Begin with API testing, as it’s the most efficient way to get started and provides broad coverage. Tools like Postman allow you to create API tests with minimal coding. Once you have the bandwidth, expand into other test types like unit and performance testing.
The Risk of Ignoring: Skipping API testing can result in a low coverage percentage and leave your application vulnerable to integration issues.
✔️ 4.2 Include 3 Parts in Each Test Name
TL;DR: Write self-explanatory test names that describe what is being tested, under what conditions, and the expected result. This clarity benefits everyone involved in the development process.
The Risk of Ignoring: Vague test names can lead to confusion and make it difficult to understand what’s failing and why, especially during critical deployments.
✔️ 4.3 Structure Tests by the AAA Pattern
TL;DR: Organize your tests with Arrange, Act, and Assert sections. This clear structure ensures that anyone reading the test can quickly understand its flow and intent.
The Risk of Ignoring: Without a clear testing structure, tests can become confusing and hard to maintain, leading to wasted time and effort.
✔️ 4.4 Ensure Node Version is Unified
TL;DR: Standardize the Node.js version across all development and production environments using tools like nvm or Volta. This prevents discrepancies that can lead to unexpected behaviors.
The Risk of Ignoring: Diverging Node.js versions can cause inconsistencies, leading to bugs that are hard to replicate and fix.
✔️ 4.5 Avoid Global Test Fixtures and Seeds, Add Data Per-Test
TL;DR: Isolate tests by having each one set up its required data. This approach prevents tests from affecting each other and makes it easier to understand each test scenario.
The Risk of Ignoring: Shared test data can lead to interdependent tests that fail unpredictably and complicate the debugging process.
✔️ 4.6 Tag Your Tests
TL;DR: Use tags to organize and selectively run tests based on the context, such as smoke tests or I/O-heavy tests. This can be done using your test runner’s filtering capabilities, like the --grep
option in Mocha.
The Risk of Ignoring: Running all tests indiscriminately can be slow and inefficient, especially when only a subset is relevant to the current development context.
✔️ 4.7 Check Your Test Coverage, It Helps to Identify Wrong Test Patterns
TL;DR: Utilize code coverage tools to track the effectiveness of your tests and ensure they cover a broad spectrum of your codebase. Set thresholds to maintain high coverage standards.
The Risk of Ignoring: Without coverage insights, you may overlook untested code paths that could contain critical bugs.
✔️ 4.8 Use Production-Like Environment for E2E Testing
TL;DR: Perform end-to-end (e2e) testing in an environment that closely mimics production. This can be achieved using containerization tools like Docker to replicate production setups.
The Risk of Ignoring: Testing in an environment that differs from production can lead to missed bugs and false confidence in the stability of your application.
✔️ 4.9 Refactor Regularly Using Static Analysis Tools
TL;DR: Employ static analysis tools to maintain code quality and facilitate regular refactoring. These tools can be integrated into your CI pipeline to flag issues automatically.
The Risk of Ignoring: Neglecting regular refactoring can lead to a codebase with mounting technical debt, making future changes more difficult and error-prone.
✔️ 4.10 Mock Responses of External HTTP Services
TL;DR: Simulate external API responses using tools like nock to ensure your tests are reliable and your application handles various network conditions correctly.
The Risk of Ignoring: Real network calls in tests can introduce flakiness and slow down your testing feedback loop.
✔️ 4.11 Test Your Middlewares in Isolation
TL;DR: When testing complex middleware logic, isolate the middleware to ensure it behaves as expected under various conditions without the need to run the entire application stack.
The Risk of Ignoring: Unisolated middleware tests can miss edge cases and lead to middleware logic that behaves unexpectedly in production.
✔️ 4.12 Specify a Port in Production, Randomize in Testing
TL;DR: Use a fixed port in production for consistency but randomize ports during testing to avoid port collisions and enable parallel test execution.
The Risk of Ignoring: Hard-coded ports can lead to conflicts and hinder the scalability and parallelization of your test suite.
✔️ 4.13 Test the Five Possible Outcomes
TL;DR: Ensure your tests cover the five potential outcomes of any action: responses, state changes, outgoing API calls, message queueing, and observability functions. Use a checklist to verify all outcomes are tested.
The Risk of Ignoring: Focusing solely on one type of outcome, such as HTTP responses, can leave other critical aspects of your application untested and prone to failure.
5. Going To Production Practices
✔️ 5.1 Monitoring
TL;DR: Effective monitoring is proactive, not reactive. Choose a monitoring solution that covers the four pillars of observability: uptime, user-facing metrics, system-level metrics, and distributed tracing. Solutions should be evaluated based on your specific needs, but make sure they cover these core areas.
The Risk of Ignoring: Without proper monitoring, you’re flying blind. Any issues that arise could lead to customer dissatisfaction before you’re even aware there’s a problem.
✔️ 5.2 Increase Observability with Smart Logging
TL;DR: Approach logging strategically by planning how logs are collected, stored, and analyzed. Ensure that your logging setup allows you to track error rates, trace transactions, and tell the story of your application’s operation.
The Risk of Ignoring: Inadequate logging practices can leave you with a “black box” application, making diagnosing issues post-deployment difficult and time-consuming.
✔️ 5.3 Delegate to a Reverse Proxy
TL;DR: Offload tasks like gzip compression and SSL termination to a reverse proxy or specialized services. Node.js’s single-threaded nature means it’s not ideal for CPU-intensive tasks.
The Risk of Ignoring: Overloading your Node.js server with non-core tasks can lead to performance bottlenecks and suboptimal response times.
✔️ 5.4 Lock Dependencies
TL;DR: Ensure consistent behavior across all environments by committing your package-lock.json
. This locks your dependencies to specific versions, preventing accidental upgrades.
The Risk of Ignoring: Unlocked dependencies can lead to the infamous “works on my machine” syndrome, where code behaves differently across environments.
✔️ 5.5 Guard Process Uptime with the Right Tool
TL;DR: Use the appropriate tools to keep your process running. Modern container orchestration systems handle this automatically, but for traditional server setups, consider using process managers like systemd.
The Risk of Ignoring: A lack of process management can result in downtime and a poor user experience during failures.
✔️ 5.6 Utilize All CPU Cores
TL;DR: Don’t let your Node.js application underutilize system resources. Ensure that your deployment strategy takes advantage of all available CPU cores, either through clustering or replication.
The Risk of Ignoring: Running a Node.js application on a single core can waste valuable server resources, leading to suboptimal performance.
✔️ 5.7 Create a ‘Maintenance Endpoint’
TL;DR: Implement a secured API endpoint that provides system health information and maintenance capabilities. This can simplify diagnostics and allow for more straightforward maintenance operations.
The Risk of Ignoring: Without a dedicated maintenance endpoint, you might find yourself frequently deploying “diagnostic code” to production, which is far from ideal.
✔️ 5.8 Discover Unknowns Using APM Products
TL;DR: Augment your monitoring with Application Performance Monitoring (APM) tools. These can provide insights into slow transactions, end-user experience, and give context for errors, going beyond traditional monitoring.
The Risk of Ignoring: Traditional monitoring might miss subtleties that APM tools can catch, potentially leaving you unaware of performance issues affecting your users.
✔️ 5.9 Make Your Code Production-Ready
TL;DR: Code with production in mind from the start. This means considering how your application will be monitored, its performance profile, security implications, and how it will scale.
The Risk of Ignoring: Failing to consider production needs early on can lead to significant rework and technical debt down the line.
✔️ 5.10 Measure and Guard Memory Usage
TL;DR: Monitor your Node.js process memory usage closely. Use monitoring tools to track memory usage and trends to catch leaks early.
The Risk of Ignoring: Memory leaks can accumulate over time, leading to slow performance and potential downtime when the process runs out of memory.
✔️ 5.11 Get Your Frontend Assets Out of Node
TL;DR: Serve static assets from a dedicated service or infrastructure. Node.js is not optimized for serving static files and is best used for dynamic content.
The Risk of Ignoring: Serving static files from Node.js can lead to inefficiencies and slow down your application’s ability to handle dynamic requests.
✔️ 5.12 Strive to Be Stateless
TL;DR: Keep your application stateless whenever possible. This simplifies scaling and improves resilience, as the loss of a single instance has a minimal impact on the overall system.
The Risk of Ignoring: Stateful applications can be challenging to scale and may lead to downtime or inconsistencies when instances fail.
✔️ 5.13 Use Tools that Automatically Detect Vulnerabilities
TL;DR: Integrate tools that scan for vulnerabilities in your dependencies into your CI/CD pipeline. This ensures you catch security issues before they hit production.
The Risk of Ignoring: Vulnerabilities can go unnoticed until they’re exploited, potentially leading to security breaches and compromised user data.
✔️ 5.14 Assign a Transaction ID to Each Log Statement
TL;DR: Use correlation IDs in your logs to trace individual requests across multiple services and components. This simplifies tracking the flow of requests and diagnosing issues.
The Risk of Ignoring: Without correlation IDs, it can be difficult to trace the path of a request that spans multiple services, complicating debugging efforts.
✔️ 5.15 Set NODE_ENV=production
TL;DR: Properly set the NODE_ENV
environment variable to ‘production’ to enable optimizations in many packages and Node.js itself.
The Risk of Ignoring: Running a Node.js application without the ‘production’ flag can result in lower performance due to the lack of optimizations.
✔️ 5.16 Design Automated, Atomic, and Zero-Downtime Deployments
TL;DR: Implement a deployment process that is fast, reliable, and doesn’t require downtime. Use containerization and CI/CD pipelines to achieve this.
The Risk of Ignoring: Manual and slow deployment processes can lead to errors, extended downtime, and a reluctance to release updates frequently.
✔️ 5.17 Use an LTS Release of Node.js
TL;DR: Stick to Long-Term Support (LTS) versions of Node.js for stability and continued support, including bug fixes and security updates.
The Risk of Ignoring: Using non-LTS releases might leave your application prone to bugs and vulnerabilities that are no longer addressed by the community.
✔️ 5.18 Log to stdout, Avoid Specifying Log Destination Within the App
TL;DR: Write logs to stdout and let the execution environment handle where logs are directed. This decouples your application code from the infrastructure and allows for more flexibility.
The Risk of Ignoring: Hardcoding log destinations can reduce flexibility and obscure valuable information if the logging system isn’t configured to handle crashes or panics effectively.
✔️ 5.19 Install Your Packages with npm ci
TL;DR: Use npm ci
for installations to ensure that your node_modules directory matches exactly what was specified in your package-lock.json
, avoiding accidental package upgrades.
The Risk of Ignoring: Using npm install
can lead to inconsistencies between development and production builds, potentially leading to “works on my machine” issues.
6. Security Best Practices
✔️ 6.1 Embrace Linter Security Rules
TL;DR: Incorporate security-focused linter plugins like eslint-plugin-security to identify and mitigate vulnerabilities during the coding process. These tools are adept at detecting issues such as dangerous eval
calls, problematic child process executions, and insecure dynamic imports.
The Risk of Ignoring: Overlooking security linting can turn minor oversights into critical security flaws in production, potentially leading to compromised systems and data breaches.
✔️ 6.2 Limit Concurrent Requests Using Middleware
TL;DR: Defend against Denial-of-Service (DoS) attacks by implementing rate limiting. Employ middleware like express-rate-limit or leverage external services like cloud load balancers and firewalls to manage the influx of requests.
The Risk of Ignoring: Without protective rate limiting, your application may fall victim to DoS attacks, disrupting service for legitimate users and potentially causing system outages.
✔️ 6.3 Extract Secrets from Config Files or Encrypt Them
TL;DR: Never store secrets in plaintext within your configuration files or source code. Utilize secret management systems like Vault, Kubernetes/Docker Secrets, or environment variables. Encrypt secrets if they must reside in source control and employ hooks to prevent accidental commits.
The Risk of Ignoring: Exposed secrets can lead to unauthorized access to your systems and data, with potentially catastrophic consequences for your business and users.
✔️ 6.4 Prevent Query Injection Vulnerabilities with ORM/ODM Libraries
TL;DR: Protect against SQL/NoSQL injection attacks by using ORM/ODM libraries that automatically handle data sanitization and support parameterized queries. Trusted libraries like Sequelize, Knex, and mongoose offer robust protection against these types of vulnerabilities.
The Risk of Ignoring: Failing to sanitize user input can lead to injection attacks, resulting in unauthorized access, data leaks, or complete system compromise.
✔️ 6.5 Collection of Generic Security Best Practices
TL;DR: Follow a broad set of security measures applicable to Node.js and beyond. These include secure coding practices, staying current with dependency updates, and adhering to the OWASP Top 10 guidelines for web application security.
✔️ 6.6 Adjust HTTP Response Headers for Enhanced Security
TL;DR: Utilize secure HTTP headers to safeguard your users against common web threats such as XSS and clickjacking. Tools like helmet can help configure these headers to bolster your application’s defenses.
The Risk of Ignoring: Misconfigured or missing HTTP headers can leave your users vulnerable to a range of web-based attacks, undermining their security and trust in your application.
✔️ 6.7 Constantly and Automatically Inspect for Vulnerable Dependencies
TL;DR: Regularly scan your project’s dependencies for known vulnerabilities using tools like npm audit or snyk. Integrate these checks into your CI/CD pipeline to ensure issues are caught before they reach production.
The Risk of Ignoring: Neglected dependencies may contain exploits that attackers can use to compromise your application, leading to potential data breaches and service disruptions.
✔️ 6.8 Protect Users’ Passwords/Secrets Using bcrypt or scrypt
TL;DR: Securely hash and salt passwords and secrets using algorithms like bcrypt
or scrypt
. This is critical for protecting user data from brute force attacks and unauthorized access.
The Risk of Ignoring: Inadequately protected passwords and secrets are prime targets for attackers, leading to unauthorized access and potentially severe security incidents.
✔️ 6.9 Escape HTML, JS, and CSS Output
TL;DR: Mitigate the risk of XSS attacks by sanitizing user-generated content before it is rendered on the client side. Employ libraries specifically designed to escape HTML, JS, and CSS to ensure that user-supplied data is safely displayed.
The Risk of Ignoring: XSS vulnerabilities can be exploited to execute malicious scripts in other users’ browsers, compromising their security and potentially leading to data theft or account takeover.
✔️ 6.10 Validate Incoming JSON Schemas
TL;DR: Ensure all incoming JSON data adheres to expected schemas. Utilize schema validation tools like jsonschema or joi to enforce data integrity and structure.
The Risk of Ignoring: Accepting unvalidated JSON can lead to malformed data processing and potentially open up your application to various injection attacks.
✔️ 6.11 Support Blocklisting JWTs
TL;DR: Implement a blocklist for JWTs to revoke tokens when necessary, such as in cases of token compromise or user deauthorization.
The Risk of Ignoring: Without a revocation mechanism, JWTs remain valid until expiration, even if they have been compromised, posing a security risk.
✔️ 6.12 Prevent Brute-Force Attacks Against Authorization
TL;DR: Implement measures to thwart brute-force attacks by limiting login attempts and using techniques such as account lockout, CAPTCHAs, or multi-factor authentication.
The Risk of Ignoring: Failing to protect against brute-force attacks can lead to compromised user accounts and unauthorized access to sensitive areas of your application.
✔️ 6.13 Run Node.js as a Non-Root User
TL;DR: Always run Node.js processes with the least privileges necessary, preferably as a non-root user, to minimize the potential impact of a security breach.
The Risk of Ignoring: Running Node.js as root can give attackers full control over the host machine if the application is compromised, leading to a much larger security incident.
✔️ 6.14 Limit Payload Size Using a Reverse-Proxy or Middleware
TL;DR: Defend against DoS attacks by restricting the size of incoming payloads. Configure limits at the reverse-proxy level or use middleware like body-parser for fine-grained control within your application.
The Risk of Ignoring: Allowing unlimited payload sizes can exhaust server resources, leading to slow performance and potential service outages.
✔️ 6.15 Avoid JavaScript Eval Statements
TL;DR: Steer clear of eval
and similar functions that execute dynamic JavaScript code, as they can be abused to run malicious scripts and compromise your application.
The Risk of Ignoring: The use of eval
can lead to severe security risks, including arbitrary code execution and XSS attacks.
✔️ 6.16 Prevent Evil Regex from Overloading Your Single Thread Execution
TL;DR: Be cautious with regex patterns that can cause excessive CPU load. Use validation libraries and tools like safe-regex to identify and avoid vulnerable regex patterns.
The Risk of Ignoring: Certain regex patterns can cause catastrophic backtracking, leading to performance issues and potential denial of service.
✔️ 6.17 Avoid Module Loading Using a Variable
TL;DR: Refrain from dynamically importing modules with variables to prevent the risk of code injection. Stick to static imports and require statements for safety and clarity.
The Risk of Ignoring: Dynamic imports based on user input can lead to arbitrary code execution vulnerabilities if not handled securely.
✔️ 6.18 Run Unsafe Code in a Sandbox
TL;DR: Isolate and execute untrusted code in a sandbox environment to limit its access to the system. Use dedicated processes, serverless environments, or sandbox packages to run such code safely.
The Risk of Ignoring: Running untrusted code without proper isolation can compromise your server and lead to data breaches or system takeover.
✔️ 6.19 Take Extra Care When Working with Child Processes
TL;DR: Sanitize inputs and use safe methods like child_process.execFile
when working with child processes to mitigate the risk of shell injection attacks.
The Risk of Ignoring: Improper handling of child processes can lead to remote command execution, giving attackers the ability to run arbitrary commands on your server.
✔️ 6.20 Hide Error Details from Clients
TL;DR: Configure your error handling to prevent sensitive details from being sent to clients. Customize error messages to avoid leaking stack traces or server configurations.
The Risk of Ignoring: Exposing error details can give attackers valuable insight into your application, aiding them in crafting targeted attacks.
✔️ 6.21 Configure 2FA for npm or Yarn
TL;DR: Secure your package management workflow with Multi-Factor Authentication (MFA) to protect against unauthorized access and malicious package alterations.
The Risk of Ignoring: Compromised package management accounts can lead to the distribution of malicious code, affecting many projects and users.
✔️ 6.22 Modify Session Middleware Settings
TL;DR: Fine-tune session middleware settings to enhance security. Avoid using default configurations that can make your application susceptible to hijacking and other session-based attacks.
The Risk of Ignoring: Default or misconfigured session settings can be exploited by attackers, potentially leading to unauthorized access and session takeover.
✔️ 6.23 Avoid DoS Attacks by Explicitly Setting When a Process Should Crash
TL;DR: Define clear rules for process termination in the event of an error. Use strategic error handling to prevent crashes from benign issues while allowing the process to terminate on critical failures.
The Risk of Ignoring: Without proper error handling, your application may be susceptible to DoS attacks that can repeatedly crash processes, leading to service outages.
✔️ 6.24 Prevent Unsafe Redirects
TL;DR: Ensure that all redirects are validated and do not rely on user input. This prevents attackers from redirecting users to malicious sites or phishing pages.
The Risk of Ignoring: Unsafe redirects can be exploited for phishing and other malicious activities, compromising user security.
✔️ 6.25 Avoid Publishing Secrets to the npm Registry
TL;DR: Take precautions to prevent secrets from being published to npm or other public registries. Use .npmignore
or the files
field in package.json
to control what gets included in your package.
The Risk of Ignoring: Accidentally published secrets can be exploited by attackers, leading to unauthorized access and potential data breaches.
✔️ 6.26 Inspect for Outdated Packages
TL;DR: Regularly check for and update outdated npm packages to mitigate the risk of vulnerabilities. Use tools like npm outdated
or npm-check-updates to automate this process.
The Risk of Ignoring: Running outdated packages may expose your application to known vulnerabilities that attackers can exploit.
✔️ 6.27 Import Built-in Modules Using the ‘node:’ Protocol
TL;DR: When importing built-in Node.js modules, use the ‘node:’ protocol prefix to clearly distinguish them from user-installed packages and prevent typosquatting attacks.
import { createServer } from "node:http";
The Risk of Ignoring: Skipping the ‘node:’ prefix can lead to confusion and potentially importing malicious packages that masquerade as built-in modules.
7. Performance Best Practices
✔️ 7.1 Don’t Block the Event Loop
TL;DR: Node.js thrives on its event-driven, non-blocking architecture. Keep CPU-bound tasks off the main event loop by offloading them to worker threads or separate processes. If you’re dealing with compute-heavy tasks, consider employing other technologies better suited for such operations.
The Risk of Ignoring: Ignoring the single-threaded nature of the event loop can lead to significant performance bottlenecks. Imagine 3,000 clients awaiting responses, but the event loop is hogged by a single, CPU-intensive request—your application’s throughput will take a substantial hit.
✔️ 7.2 Prefer Native JS Methods Over User-Land Utils Like Lodash
TL;DR: Modern JavaScript engines and the evolving ECMAScript standards have significantly optimized native functions, making them more performant than utility libraries like Lodash in many cases. Before reaching for an external utility, evaluate whether native JS methods can fulfill your needs—they’ll likely lead to faster, more efficient code.
The Risk of Ignoring: Overreliance on utility libraries can introduce unnecessary dependencies and overhead. By opting for native methods, you’re not only embracing modern JavaScript improvements but also reducing bloat and potential performance penalties.
8. Docker Best Practices
🏅 Special thanks to Bret Fisher, whose insights have informed many of the practices below.
✔️ 8.1 Use Multi-Stage Builds for Leaner and More Secure Docker Images
TL;DR: Employ multi-stage builds to copy only the artifacts needed for production, avoiding unnecessary build dependencies and files. This results in slimmer images and reduces potential security risks.
The Risk of Ignoring: Heavier images lead to longer build and deployment times, while unnecessary build tools and files can introduce vulnerabilities.
Example Dockerfile for Multi-Stage Builds
# Build stage
FROM node:14.4.0 AS build
WORKDIR /app
COPY . .
RUN npm ci && npm run build
# Production stage
FROM node:14.4.0-slim
WORKDIR /app
COPY --from=build /app/dist ./
COPY package*.json ./
RUN npm ci --production
USER node
EXPOSE 8080
CMD ["node", "app.js"]
✔️ 8.2 Bootstrap Using node
Command, Avoid npm start
TL;DR: Start your Node.js app with CMD ["node", "app.js"]
for proper signal handling and graceful shutdowns. Avoid npm start
in production as it can obscure signals and complicate process management.
Update: As of npm 7, npm claims to properly pass signals. Monitor this and adjust practices accordingly.
The Risk of Ignoring: Ignoring proper signal handling can lead to unresponsive containers and difficulties in orchestrating graceful shutdowns and restarts.
✔️ 8.3 Let the Docker Runtime Handle Replication and Uptime
TL;DR: When using a container orchestrator like Kubernetes, let it manage the replication and lifecycle of Node.js processes. Avoid custom process managers that can obscure container lifecycle events and signals.
The Risk of Ignoring: Mismanaged container processes can lead to inefficient scaling and handling of containers, potentially causing service disruptions and resource wastage.
✔️ 8.4 Use .dockerignore
to Prevent Leaking Secrets
TL;DR: Include a .dockerignore
file to exclude files that may contain secrets or are not necessary for building the image, such as .env
files or development-specific configurations.
The Risk of Ignoring: Secrets can be inadvertently included in Docker images, posing a security risk if the images are pushed to public registries.
✔️ 8.5 Clean-up Dependencies Before Production
TL;DR: Remove development dependencies in your Docker images to minimize the attack surface and reduce image size. This can be achieved with multi-stage builds and by running npm ci --production
.
The Risk of Ignoring: Development dependencies can include vulnerabilities that are not needed in production, increasing the risk of security breaches.
✔️ 8.6 Shutdown Smartly and Gracefully
TL;DR: Handle SIGTERM
signals within your Node.js applications to close servers and release resources properly. This ensures that the container can be stopped and restarted gracefully.
The Risk of Ignoring: Abrupt shutdowns can lead to incomplete transactions, resource leaks, and can disrupt the user experience.
✔️ 8.7 Set Memory Limits Using Both Docker and V8
TL;DR: Define memory limits in both the Docker configuration and the Node.js runtime to ensure efficient garbage collection and resource allocation.
The Risk of Ignoring: Without setting memory limits, Node.js applications can either underutilize resources or exceed container limits, leading to terminated instances.
✔️ 8.8 Plan for Efficient Caching
TL;DR: Optimize Docker layer caching by ordering Dockerfile instructions from the least frequently changed to the most frequently changed.
The Risk of Ignoring: Inefficient caching can result in longer build times and increased resource consumption.
✔️ 8.9 Use Explicit Image References, Avoid latest
Tag
TL;DR: Specify explicit image tags or SHAs to ensure reproducibility and avoid unexpected changes from updated base images.
The Risk of Ignoring: Using the latest
tag can lead to inconsistencies and unexpected behavior if the latest
image changes.
✔️ 8.10 Prefer Smaller Docker Base Images
TL;DR: Use smaller base images, like Alpine or Slim variants, to reduce build times, minimize security risks, and decrease resource usage.
The Risk of Ignoring: Larger base images can include unnecessary components that increase the potential attack surface and consume more resources.
✔️ 8.11 Clean Out Build-Time Secrets and Avoid Secrets in Args
TL;DR: Remove secrets used during the build process and avoid passing them in build arguments. Utilize multi-stage builds and Docker secrets to manage sensitive information securely.
The Risk of Ignoring: Build-time secrets can remain in the image layers and be exposed to users or other applications.
✔️ 8.12 Scan Images for Multiple Layers of Vulnerabilities
TL;DR: Perform comprehensive security scans on your Docker images to catch vulnerabilities not only in your application but also in the underlying OS and libraries.
The Risk of Ignoring: Focusing solely on application dependencies might miss critical vulnerabilities in the operating system or other layers.
✔️ 8.13 Clean Node Module Cache
TL;DR: Clear the npm cache after installing dependencies in your Dockerfile to reduce image size and avoid unnecessary duplication.
The Risk of Ignoring: Leaving the npm cache in your image results in larger image sizes and potentially slower deployments.
✔️ 8.14 Generic Docker Practices
TL;DR: Apply a set of general Docker practices that are not specific to Node.js but are essential for a secure and efficient containerized environment.
✔️ 8.15 Lint Your Dockerfile
TL;DR: Use Docker linters to identify potential issues in your Dockerfile, ensuring adherence to best practices and avoiding common mistakes.
The Risk of Ignoring: Unlinted Dockerfiles can lead to security and performance issues that could be easily avoided with proper linting.
For an in-depth exploration of Node.js best practices and to engage with the community, visit the original repository at Node.js Best Practices.
By following these best practices, you’ll create Node.js applications that are not only well-structured and readable but also robust, secure, and performant. These guidelines will help you navigate the complexities of modern Node.js development and ensure that your applications are ready for the challenges of production environments.
- Tags:
- #Nodejs