DEV Community

Danilo Itagyba
Danilo Itagyba

Posted on

๐Ÿš€ Node.js Best Practices

Node.js Best Practices

Comprehensive Node.js development guidelines
Based on goldbergyoni/nodebestpractices

๐ŸŽฏ 102 Best Practices | ๐Ÿ“ฆ Node.js 22+ |

Table of Contents

  1. Project Structure
  2. Framework Selection
  3. TypeScript Usage
  4. Error Handling
  5. Code Style and Patterns
  6. Testing and Quality
  7. Production Best Practices
  8. Security Best Practices
  9. Docker Best Practices
  10. Code Examples
  11. Summary Checklist

Project Structure

๐Ÿ“‚ Component-Based Architecture

Structure your application by business components (bounded contexts) for better modularity and maintainability.

my-system/
โ”œโ”€โ”€ apps/                    # Business components
โ”‚   โ”œโ”€โ”€ orders/
โ”‚   โ”œโ”€โ”€ users/
โ”‚   โ”œโ”€โ”€ payments/
โ””โ”€โ”€ libraries/               # Shared utilities
    โ”œโ”€โ”€ logger/
    โ””โ”€โ”€ authenticator/
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • โœ… Reduced mental overload
  • โœ… Smaller, granular changes
  • โœ… Faster development cycles
  • โœ… Clear modularity borders

Anti-pattern:

  • โŒ Mixing artifacts from different modules
  • โŒ Tightly-coupled 'spaghetti' architecture
  • โŒ Global dependencies everywhere

Best Practices:

  • โœ… Each module has its own package.json
  • โœ… Use package.json.main or package.json.exports for public API
  • โœ… Hide internal implementation details
  • โœ… Support Monorepo or multi-repo approaches

โš™๏ธ Configuration Management

Implement secure, flexible configuration from day one.

Requirements:

  • โœ… Read from files AND environment variables
  • โœ… Keep secrets outside committed code
  • โœ… Hierarchical structure for easy navigation
  • โœ… Type support and validation
  • โœ… Default values for all keys

Recommended Libraries:

// Example with convict
const convict = require('convict');

const config = convict({
  env: {
    format: ['production', 'development', 'test'],
    default: 'development',
    env: 'NODE_ENV'
  },
  port: {
    format: 'port',
    default: 3000,
    env: 'PORT'
  }
});

config.validate({ allowed: 'strict' });
Enter fullscreen mode Exit fullscreen mode

Framework Selection

Recommended Frameworks (2024)

  1. Nest.js - For OOP teams and large-scale apps
  2. Fastify - For microservices with simple Node.js mechanics
  3. Express - For simplicity and wide ecosystem
  4. Koa - For minimalist, modern middleware approach

Framework Considerations

  • Evaluate based on team size, project scale, and architecture
  • Consider TypeScript support and plugin ecosystem
  • Assess performance requirements vs development speed

TypeScript Usage

Use TypeScript Sparingly and Thoughtfully

  • Define variables and function return types
  • Avoid over-engineering with advanced features
  • Use ~50 keywords thoughtfully, not excessively
  • Utilize advanced features only when truly needed
  • Remember: Types catch ~20% of bugs; tests catch the rest
  • Balance type safety with code simplicity

Error Handling

Async Error Handling

  • Use async-await or promises exclusively
  • Avoid callback-style error handling (callback hell)
  • Enable better error handling with try-catch blocks

Error Object Standards

  • Always extend the built-in Error object
  • Add useful properties: name, code, isCatastrophic
  • Use ESLint rule: no-throw-literal
  • For TypeScript: @typescript-eslint/no-throw-literal

Error Classification

  • Operational Errors: Known, expected errors (invalid input)
    • Handle gracefully, log, and continue
  • Catastrophic Errors: Unknown programmer errors
    • Log, alert, and restart process gracefully

Centralized Error Handling

  • Create dedicated error handler object
  • Don't handle errors within middleware
  • Encapsulate: logging, crash decisions, monitoring
  • All entry points should use the same handler

Error Documentation

  • Document API errors using OpenAPI or GraphQL
  • Help API callers handle errors properly
  • Prevent unexpected crashes in client applications

Process Exit Strategy

  • Exit gracefully on unknown errors
  • Let runtime (Docker, Kubernetes) restart the process
  • Log observable errors before shutdown
  • Close connections properly

Logging Best Practices

  • Use mature logger: Pino or Winston (avoid console.log)
  • Support log levels, pretty printing, and coloring
  • Write logs to stdout (let infrastructure handle routing)
  • Attach custom properties with minimal serialization penalty

Testing Error Flows

  • Test both positive and error scenarios
  • Simulate uncaught exceptions
  • Verify error handler behavior
  • Test error recovery mechanisms

Monitoring

  • Use APM products to discover errors automatically
  • Monitor performance and downtime proactively
  • Gauge application health in real-time

Promise Management

  • Catch unhandled promise rejections
  • Register to process.unhandledRejection event
  • Always await promises before returning for full stacktrace
  • Use return await pattern in async functions

Input Validation

  • Fail fast: validate arguments immediately
  • Use validation libraries: ajv, zod, typebox
  • Validate at API boundaries
  • Assert API input to prevent nasty bugs

Event Emitter Error Handling

  • Subscribe to 'error' events on event emitters
  • Handle stream errors explicitly
  • For EventTargets, use global process error handling
  • Initialize emitters with {captureRejections: true} for async handlers

Code Style and Patterns

ESLint Configuration

  • Use ESLint as the standard linter
  • Combine with Prettier for code formatting
  • Catch errors and anti-patterns early

Node.js-Specific Linting

  • Use plugins:
    • eslint-plugin-node
    • eslint-plugin-security
    • eslint-plugin-mocha / eslint-plugin-jest
    • eslint-plugin-require
  • Detect Node.js-specific anti-patterns

Code Formatting Rules

  • Opening braces on same line as statement
  • Proper statement separation (use semicolons or understand ASI)
  • Use ESLint semi rule
  • Use no-unexpected-multiline rule

Naming Conventions

  • lowerCamelCase: variables, constants, functions
  • UpperCamelCase: classes
  • UPPER_SNAKE_CASE: global/static variables
  • Name all functions (avoid anonymous functions)
  • Use descriptive but concise names

Variable Declaration

  • Prefer const over let
  • Avoid var completely (ES6+)
  • const prevents accidental reassignment
  • let for block-scoped variables that need reassignment

Module Management

  • Require modules at file beginning
  • Import before any function definitions
  • Avoid requires inside functions (except lazy loading)

Module Entry Points

  • Set explicit entry point (index.js or package.json.main)
  • Use package.json.exports for ESM
  • Hide internal structure from clients

Comparison Operators

  • Use strict equality === over ==
  • Avoid type coercion surprises

Async Patterns

  • Use async-await as primary pattern
  • Avoid callbacks (callback hell)
  • Promises for library compatibility
  • Arrow functions for compact code

Side Effects

  • Avoid effects outside of functions
  • No DB/network calls at module load time
  • Use factory or revealing module patterns for initialization
  • Keep code testable and mockable

Testing and Quality

Test Coverage Requirements

  • Write API (component) tests at minimum
  • Component tests provide more coverage than unit tests
  • Use tools like Postman for API testing

Test Naming Convention (3 Parts)

  1. What is being tested (unit under test)
  2. Under what circumstances
  3. What is the expected result
  4. Example: "ProductService - when no price provided - should return validation error"

Test Structure (AAA Pattern)

  1. Arrange: Setup test data and conditions
  2. Act: Execute the unit under test
  3. Assert: Verify expected outcome

Environment Consistency

  • Use Node version management: nvm or Volta
  • Specify Node version in project files
  • Replicate to CI and production runtime
  • Prevent version drift across environments

Test Data Management

  • Avoid global test fixtures and seeds
  • Add data per-test (test isolation)
  • Each test manages its own database state
  • Prevent test coupling and interference

Test Organization

  • Tag tests by category: #cold, #api, #sanity, #integration
  • Run subsets based on context (commit, PR, deploy)
  • Example: mocha --grep 'sanity'

Code Coverage

  • Use Istanbul/NYC for coverage reports
  • Identify untested code paths
  • Set coverage thresholds in CI
  • Fail builds on coverage decrease

E2E Testing

  • Use production-like environment
  • Leverage docker-compose for services
  • Keep DBs synchronized across environments

Static Analysis

  • Use SonarQube or Code Climate
  • Detect code smells and complexity
  • Track code quality over time
  • Integrate into CI pipeline

External Service Mocking

  • Mock external HTTP services: nock or Mock-Server
  • Simulate errors, delays, timeouts
  • Test non-happy path flows
  • Avoid hitting real external services in tests

Middleware Testing

  • Test middlewares in isolation
  • Stub {req, res, next} objects
  • Verify middleware behavior independently

Port Management

  • Specify port in production
  • Randomize port in testing (port 0)
  • Prevent port collisions in parallel tests

Test Five Outcomes

  1. Response validation
  2. State changes (DB, memory)
  3. Outgoing API calls
  4. Message queue events
  5. Observability (logs, metrics)

Production Best Practices

Monitoring Strategy

  • Monitor uptime, metrics, and user-facing symptoms
  • Track Node.js-specific metrics (event loop lag)
  • Use distributed tracing with Open Telemetry
  • Implement comprehensive logging
  • 4 layers: uptime, metrics, distributed flows, logs

Smart Logging

  • Plan logging platform from day 1
  • Log to stdout, let environment handle routing
  • Include transaction IDs (correlation IDs)
  • Use AsyncLocalStorage for context propagation
  • Enable log aggregation and analysis

Reverse Proxy Delegation

  • Delegate CPU-intensive tasks (gzip, SSL)
  • Use nginx, HAproxy, or cloud services
  • Keep Node thread focused on application logic

Dependency Management

  • Lock dependencies with package-lock.json
  • Commit lockfiles to version control
  • Use npm ci for consistent installs
  • Ensure identical code across environments

Process Management

  • Use platform-native solutions (Kubernetes, systemd)
  • Avoid custom process managers in modern platforms
  • Let infrastructure handle restarts and health checks
  • Ensure process uptime with proper tools

CPU Utilization

  • Replicate Node process for all CPU cores
  • Use container orchestration for scaling
  • Don't rely on Node cluster module alone
  • Verify all cores are utilized

Maintenance Endpoints

  • Expose system information securely
  • Provide memory usage, health checks
  • Enable diagnostic operations
  • Keep separate from public API

APM Integration

  • Add Application Performance Monitoring
  • Auto-discover slow transactions
  • Identify performance bottlenecks
  • Provide developer context for errors

Production-Ready Code

  • Plan for production from day 1
  • Consider maintainability early
  • Design for operations and debugging
  • Build with monitoring in mind

Memory Management

  • Watch Node process memory usage
  • V8 has soft limits (1.4GB default)
  • Monitor for memory leaks
  • Alert on unusual memory growth

Frontend Asset Management

  • Serve frontend via nginx, S3, or CDN
  • Don't serve static files through Node
  • Exception: server-side rendering
  • Keep Node focused on dynamic content

Stateless Architecture

  • Store data in external data stores
  • Avoid in-process state (sessions, cache, files)
  • Enable easy horizontal scaling
  • Support instance reapp-ing

Vulnerability Detection

  • Use automated security scanning tools
  • Monitor dependencies continuously
  • Patch vulnerabilities immediately
  • Scan locally and in CI/CD

Transaction ID Logging

  • Assign UUID to each request
  • Use AsyncLocalStorage for context
  • Include in all log statements
  • Enable request flow tracing

Environment Configuration

  • Set NODE_ENV=production
  • Enable production optimizations
  • Libraries use environment for behavior
  • Affects performance significantly

Deployment Strategy

  • Automate deployments completely
  • Use Docker with CI/CD tools
  • Implement zero-downtime deployments
  • Make deployments atomic and reversible

Node.js Version

  • Use LTS (Long Term Support) releases
  • Receive critical bug fixes and security updates
  • Ensure module compatibility
  • Plan upgrade paths

Log Routing

  • Log to stdout exclusively
  • Let execution environment handle destinations
  • Don't hard-code log destinations
  • Support flexible log aggregation

Security Best Practices

Linter Security Rules

  • Use eslint-plugin-security
  • Catch vulnerabilities during development
  • Prevent eval usage and code injection
  • Detect insecure patterns early

Rate Limiting

  • Implement request limiting
  • Prevent DOS attacks
  • Use cloud load balancers, nginx, or rate-limiter-flexible
  • Protect against abuse

Secret Management

  • Never store plain-text secrets in code
  • Use Vault, Kubernetes Secrets, or environment variables
  • Encrypt secrets in source control
  • Implement pre-commit hooks
  • Rotate keys regularly

Query Injection Prevention

  • Use ORM/ODM libraries (Sequelize, Knex, Mongoose)
  • Leverage parameterized queries
  • Validate user input for expected types
  • Never concatenate user input into queries

Security Checklist

  • Enable HTTPS everywhere
  • Use helmet.js for HTTP headers
  • Implement CSRF protection
  • Sanitize user input
  • Use secure session cookies
  • Implement proper authentication
  • Apply principle of least privilege

Dependency Security

  • Scan for vulnerable dependencies
  • Use npm audit or Snyk
  • Update dependencies regularly
  • Monitor security advisories

Input Validation

  • Validate all user input
  • Use validation libraries (joi, yup, zod)
  • Implement allow-lists over deny-lists
  • Validate data types and formats

Authentication Best Practices

  • Use bcrypt or scrypt for passwords
  • Implement 2FA where possible
  • Use secure JWT practices
  • Rotate secrets regularly

Data Protection

  • Escape HTML, JS, and CSS output
  • Validate JSON schemas
  • Implement proper access controls
  • Encrypt sensitive data at rest

Secure Headers

  • Use helmet.js middleware
  • Set Content-Security-Policy
  • Enable HSTS
  • Disable X-Powered-By

Error Information Leakage

  • Hide error details from clients
  • Log detailed errors server-side
  • Return generic error messages
  • Don't expose stack traces in production

Child Process Safety

  • Avoid child_exec with user input
  • Sanitize all arguments
  • Use execFile over exec
  • Implement timeouts

RegEx DOS Prevention

  • Validate regex patterns
  • Set timeouts for regex operations
  • Test regex with long inputs
  • Use safe-regex library

Session Security

  • Use secure session configuration
  • Set httpOnly and secure flags
  • Implement session timeouts
  • Rotate session IDs

NPM Security

  • Enable 2FA for npm account
  • Audit packages before publishing
  • Use .npmignore to prevent secret leaks
  • Review dependencies regularly

Module Loading Security

  • Avoid dynamic requires with user input
  • Use static imports when possible
  • Validate module paths
  • Implement sandbox for untrusted code

Redirect Safety

  • Validate redirect URLs
  • Use allow-list for redirects
  • Prevent open redirect vulnerabilities

Built-in Module Usage

  • Import built-in modules with 'node:' protocol
  • Example: import fs from 'node:fs'
  • Makes built-in modules explicit
  • Prevents shadowing attacks

Docker Best Practices

Multi-Stage Builds

  • Use multi-stage builds for smaller images
  • Separate build and runtime dependencies
  • Reduce attack surface
  • Improve security and performance

Process Bootstrap

  • Use node command directly, avoid npm start
  • Faster startup, proper signal handling
  • No unnecessary npm process overhead

Container Runtime

  • Let orchestrator handle replication
  • Use Kubernetes/Docker Swarm for uptime
  • Don't run process manager inside container

Secret Protection

  • Use .dockerignore effectively
  • Never commit secrets to images
  • Use Docker secrets or environment variables
  • Clean secrets after build

Dependency Cleanup

  • Run npm ci --production in final stage
  • Remove devDependencies
  • Clean npm cache
  • Minimize image size

Graceful Shutdown

  • Handle SIGTERM properly
  • Implement health checks
  • Clean up connections on shutdown
  • Drain requests before exit

Memory Limits

  • Set Docker memory limits
  • Configure V8 max-old-space-size
  • Prevent OOM kills
  • Match container and V8 limits

Image Caching

  • Order Dockerfile for optimal caching
  • Copy package.json before source code
  • Leverage layer caching
  • Minimize cache invalidation

Image Tagging

  • Use explicit image references
  • Avoid 'latest' tag in production
  • Pin dependencies to versions
  • Ensure reproducible builds

Base Image Selection

  • Prefer Alpine or slim variants
  • Minimize base image size
  • Reduce vulnerabilities
  • Use official Node.js images

Build Secrets

  • Use BuildKit secret mounts
  • Never use ARG for secrets
  • Secrets don't persist in layers
  • Clean build-time secrets

Image Scanning

  • Scan for vulnerabilities (Snyk, Trivy)
  • Check multiple layers
  • Integrate into CI/CD
  • Fix critical vulnerabilities

Cache Management

  • Clean node_modules cache
  • Remove unnecessary files
  • Use .dockerignore
  • Minimize layer size

Dockerfile Linting

  • Use hadolint for linting
  • Follow best practices
  • Catch common mistakes
  • Integrate into CI

Code Examples

Proper Error Handling

// Do: Use AppError extending Error
class AppError extends Error {
  constructor(name, message, httpCode, isOperational = true) {
    super(message);
    this.name = name;
    this.httpCode = httpCode;
    this.isOperational = isOperational;
    Error.captureStackTrace(this);
  }
}

// Usage
throw new AppError('ValidationError', 'Invalid email', 400);
Enter fullscreen mode Exit fullscreen mode

Async-Await Pattern

// Do: Use async-await
async function getUser(id) {
  try {
    const user = await userRepository.findById(id);
    return user;
  } catch (error) {
    logger.error('Failed to get user', { id, error });
    throw new AppError('UserNotFound', 'User not found', 404);
  }
}

// Avoid: Callbacks
function getUser(id, callback) {
  userRepository.findById(id, (err, user) => {
    if (err) return callback(err);
    callback(null, user);
  });
}
Enter fullscreen mode Exit fullscreen mode

Configuration Management

// Do: Use convict or similar
const convict = require('convict');

const config = convict({
  env: {
    format: ['production', 'development', 'test'],
    default: 'development',
    env: 'NODE_ENV'
  },
  port: {
    format: 'port',
    default: 3000,
    env: 'PORT'
  },
  db: {
    host: {
      format: String,
      default: 'localhost',
      env: 'DB_HOST'
    }
  }
});

config.validate({ allowed: 'strict' });
module.exports = config.get();
Enter fullscreen mode Exit fullscreen mode

Logging with Context

// Do: Use AsyncLocalStorage for transaction ID
const { AsyncLocalStorage } = require('async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();

function logWithContext(message, data) {
  const context = asyncLocalStorage.getStore();
  logger.info(message, { 
    ...data, 
    transactionId: context?.transactionId 
  });
}

// Middleware to set context
app.use((req, res, next) => {
  const transactionId = uuid.v4();
  asyncLocalStorage.run({ transactionId }, () => {
    next();
  });
});
Enter fullscreen mode Exit fullscreen mode

Input Validation

// Do: Use validation library
const Joi = require('joi');

const userSchema = Joi.object({
  email: Joi.string().email().required(),
  password: Joi.string().min(8).required(),
  age: Joi.number().integer().min(18).max(120)
});

function validateUser(userData) {
  const { error, value } = userSchema.validate(userData);
  if (error) {
    throw new AppError('ValidationError', error.message, 400);
  }
  return value;
}
Enter fullscreen mode Exit fullscreen mode

Security Headers

// Do: Use helmet
const helmet = require('helmet');

app.use(helmet());
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    styleSrc: ["'self'", "'unsafe-inline'"],
    scriptSrc: ["'self'"],
    imgSrc: ["'self'", 'data:', 'https:']
  }
}));
Enter fullscreen mode Exit fullscreen mode

Summary Checklist

  • [ ] Structure by business components
  • [ ] Use 3-tier layering (entry-points, domain, data-access)
  • [ ] Implement centralized error handling
  • [ ] Use async-await exclusively
  • [ ] Configure ESLint with security plugins
  • [ ] Write API tests minimum
  • [ ] Use mature logger (Pino/Winston)
  • [ ] Implement rate limiting
  • [ ] Validate all user input
  • [ ] Lock dependencies
  • [ ] Use environment variables for config
  • [ ] Monitor with APM tools
  • [ ] Set NODE_ENV=production
  • [ ] Log to stdout only
  • [ ] Use Docker multi-stage builds
  • [ ] Implement graceful shutdown
  • [ ] Scan for vulnerabilities
  • [ ] Use LTS Node.js version
  • [ ] Enable TypeScript thoughtfully
  • [ ] Test error flows

References


Top comments (0)