Skip to main content

Command Palette

Search for a command to run...

Node.js Best Practices: Production Patterns

Published
8 min read
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Why Traditional Node.js Patterns Fail at Scale

Early Node.js patterns emerged when monolithic architectures dominated and traffic patterns were predictable. Developers relied on simple process.on('uncaughtException') handlers and basic console logging. These approaches collapse under modern constraints.

Container orchestration platforms like Kubernetes expect applications to respond to SIGTERM signals within 30 seconds. Cloud-native environments require structured logging for centralized aggregation. Distributed systems demand correlation IDs to trace requests across service boundaries. AI-driven applications processing real-time data streams cannot tolerate blocking operations or inefficient memory usage.

The shift to serverless and edge computing introduces additional complexity. Cold starts, ephemeral execution contexts, and pay-per-invocation pricing models require fundamentally different optimization strategies. Traditional singleton patterns and in-memory caching become anti-patterns when instances spin up and down dynamically.

Production-Grade Error Handling Architecture

Error handling in production Node.js requires layered defense mechanisms. The architecture must distinguish between operational errors (expected failures like network timeouts) and programmer errors (bugs requiring immediate attention).

// error-types.ts
export class OperationalError extends Error {
  constructor(
    message: string,
    public readonly statusCode: number = 500,
    public readonly isOperational: boolean = true,
    public readonly context?: Record<string, unknown>
  ) {
    super(message);
    this.name = this.constructor.name;
    Error.captureStackTrace(this, this.constructor);
  }
}

export class ValidationError extends OperationalError {
  constructor(message: string, context?: Record<string, unknown>) {
    super(message, 400, true, context);
  }
}

export class DatabaseError extends OperationalError {
  constructor(message: string, context?: Record<string, unknown>) {
    super(message, 503, true, context);
  }
}

Implement a centralized error handler that processes all errors consistently:

// error-handler.ts
import { Request, Response, NextFunction } from 'express';
import { logger } from './observability/logger';
import { metrics } from './observability/metrics';

export class ErrorHandler {
  private static instance: ErrorHandler;

  private constructor() {}

  static getInstance(): ErrorHandler {
    if (!ErrorHandler.instance) {
      ErrorHandler.instance = new ErrorHandler();
    }
    return ErrorHandler.instance;
  }

  handleError(
    error: Error,
    req?: Request,
    res?: Response,
    next?: NextFunction
  ): void {
    const isOperational = this.isOperationalError(error);

    // Increment error metrics
    metrics.incrementCounter('errors_total', {
      type: error.name,
      operational: isOperational.toString(),
      path: req?.path || 'unknown'
    });

    // Structured logging with context
    logger.error('Application error', {
      error: {
        name: error.name,
        message: error.message,
        stack: error.stack,
        operational: isOperational
      },
      request: req ? {
        method: req.method,
        path: req.path,
        correlationId: req.headers['x-correlation-id'],
        userId: (req as any).user?.id
      } : undefined
    });

    if (res && !res.headersSent) {
      const statusCode = (error as any).statusCode || 500;
      res.status(statusCode).json({
        error: {
          message: isOperational ? error.message : 'Internal server error',
          correlationId: req?.headers['x-correlation-id'],
          timestamp: new Date().toISOString()
        }
      });
    }

    // Programmer errors should trigger alerts and potentially crash
    if (!isOperational) {
      logger.fatal('Non-operational error detected', { error });
      // Allow time for logs to flush before exit
      setTimeout(() => process.exit(1), 1000);
    }
  }

  private isOperationalError(error: Error): boolean {
    if (error instanceof OperationalError) {
      return error.isOperational;
    }
    return false;
  }
}

// Express middleware
export const errorMiddleware = (
  error: Error,
  req: Request,
  res: Response,
  next: NextFunction
) => {
  ErrorHandler.getInstance().handleError(error, req, res, next);
};

Graceful Shutdown and Signal Handling

Kubernetes and modern orchestrators send SIGTERM signals before forcefully killing pods. Applications must drain existing connections, complete in-flight requests, and release resources cleanly.

// shutdown-manager.ts
import { Server } from 'http';
import { logger } from './observability/logger';

export class ShutdownManager {
  private isShuttingDown = false;
  private readonly shutdownTimeout: number;
  private readonly components: Map<string, () => Promise<void>>;

  constructor(shutdownTimeoutMs: number = 25000) {
    this.shutdownTimeout = shutdownTimeoutMs;
    this.components = new Map();
  }

  registerComponent(name: string, cleanup: () => Promise<void>): void {
    this.components.set(name, cleanup);
  }

  async gracefulShutdown(signal: string): Promise<void> {
    if (this.isShuttingDown) {
      logger.warn('Shutdown already in progress');
      return;
    }

    this.isShuttingDown = true;
    logger.info(`Received ${signal}, starting graceful shutdown`);

    const shutdownPromise = this.executeShutdown();
    const timeoutPromise = new Promise((_, reject) =>
      setTimeout(() => reject(new Error('Shutdown timeout')), this.shutdownTimeout)
    );

    try {
      await Promise.race([shutdownPromise, timeoutPromise]);
      logger.info('Graceful shutdown completed');
      process.exit(0);
    } catch (error) {
      logger.error('Shutdown failed or timed out', { error });
      process.exit(1);
    }
  }

  private async executeShutdown(): Promise<void> {
    const shutdownPromises: Promise<void>[] = [];

    for (const [name, cleanup] of this.components.entries()) {
      shutdownPromises.push(
        cleanup()
          .then(() => logger.info(`${name} shutdown complete`))
          .catch((error) => logger.error(`${name} shutdown failed`, { error }))
      );
    }

    await Promise.allSettled(shutdownPromises);
  }

  setupSignalHandlers(): void {
    const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT'];

    signals.forEach((signal) => {
      process.on(signal, () => {
        this.gracefulShutdown(signal);
      });
    });

    // Handle uncaught errors
    process.on('uncaughtException', (error) => {
      logger.fatal('Uncaught exception', { error });
      this.gracefulShutdown('uncaughtException');
    });

    process.on('unhandledRejection', (reason, promise) => {
      logger.fatal('Unhandled rejection', { reason, promise });
      this.gracefulShutdown('unhandledRejection');
    });
  }
}

// Usage in application bootstrap
export async function bootstrap() {
  const app = express();
  const server = app.listen(3000);

  const shutdownManager = new ShutdownManager();

  // Register HTTP server
  shutdownManager.registerComponent('http-server', async () => {
    return new Promise((resolve, reject) => {
      server.close((err) => {
        if (err) reject(err);
        else resolve();
      });
    });
  });

  // Register database connections
  shutdownManager.registerComponent('database', async () => {
    await prisma.$disconnect();
  });

  // Register Redis connections
  shutdownManager.registerComponent('redis', async () => {
    await redis.quit();
  });

  shutdownManager.setupSignalHandlers();
}

Observability and Structured Logging

Production systems require comprehensive observability: structured logs, metrics, and distributed tracing. OpenTelemetry has become the standard for instrumentation in 2025.

// observability/logger.ts
import pino from 'pino';

export const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level: (label) => {
      return { level: label };
    },
    bindings: (bindings) => {
      return {
        pid: bindings.pid,
        host: bindings.hostname,
        service: process.env.SERVICE_NAME || 'unknown',
        environment: process.env.NODE_ENV || 'development'
      };
    }
  },
  serializers: {
    req: pino.stdSerializers.req,
    res: pino.stdSerializers.res,
    err: pino.stdSerializers.err
  },
  timestamp: pino.stdTimeFunctions.isoTime
});

// observability/tracing.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';

export function initializeTracing() {
  const sdk = new NodeSDK({
    resource: new Resource({
      [SemanticResourceAttributes.SERVICE_NAME]: process.env.SERVICE_NAME || 'node-service',
      [SemanticResourceAttributes.SERVICE_VERSION]: process.env.SERVICE_VERSION || '1.0.0',
      [SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV || 'development'
    }),
    traceExporter: new OTLPTraceExporter({
      url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318/v1/traces'
    }),
    instrumentations: [
      getNodeAutoInstrumentations({
        '@opentelemetry/instrumentation-fs': { enabled: false },
        '@opentelemetry/instrumentation-http': {
          ignoreIncomingPaths: ['/health', '/metrics']
        }
      })
    ]
  });

  sdk.start();

  process.on('SIGTERM', () => {
    sdk.shutdown()
      .then(() => logger.info('Tracing terminated'))
      .catch((error) => logger.error('Error terminating tracing', { error }));
  });
}

Performance Optimization Patterns

Node.js performance in production requires attention to event loop health, memory management, and efficient I/O patterns.

// performance/event-loop-monitor.ts
import { performance, PerformanceObserver } from 'perf_hooks';
import { logger } from '../observability/logger';
import { metrics } from '../observability/metrics';

export class EventLoopMonitor {
  private readonly threshold: number;
  private lastCheck: number = Date.now();

  constructor(thresholdMs: number = 100) {
    this.threshold = thresholdMs;
    this.startMonitoring();
  }

  private startMonitoring(): void {
    setInterval(() => {
      const now = Date.now();
      const delay = now - this.lastCheck - 1000; // Expected 1000ms interval

      if (delay > this.threshold) {
        logger.warn('Event loop delay detected', { delayMs: delay });
        metrics.recordHistogram('event_loop_delay_ms', delay);
      }

      this.lastCheck = now;
    }, 1000);

    // Monitor async operations
    const obs = new PerformanceObserver((items) => {
      items.getEntries().forEach((entry) => {
        if (entry.duration > this.threshold) {
          logger.warn('Slow async operation', {
            name: entry.name,
            duration: entry.duration
          });
        }
      });
    });

    obs.observe({ entryTypes: ['measure'] });
  }
}

// performance/connection-pooling.ts
import { Pool } from 'pg';

export function createOptimizedPool() {
  return new Pool({
    max: parseInt(process.env.DB_POOL_MAX || '20'),
    min: parseInt(process.env.DB_POOL_MIN || '5'),
    idleTimeoutMillis: 30000,
    connectionTimeoutMillis: 5000,
    maxUses: 7500, // Rotate connections to prevent memory leaks
    allowExitOnIdle: false,

    // Connection validation
    application_name: process.env.SERVICE_NAME,
    statement_timeout: 30000,
    query_timeout: 30000
  });
}

Configuration Management and Secrets

Production applications require secure configuration management with environment-specific overrides and secret rotation support.

// config/config-manager.ts
import { z } from 'zod';

const configSchema = z.object({
  nodeEnv: z.enum(['development', 'staging', 'production']),
  port: z.number().int().positive(),
  logLevel: z.enum(['debug', 'info', 'warn', 'error', 'fatal']),
  database: z.object({
    host: z.string(),
    port: z.number().int().positive(),
    name: z.string(),
    user: z.string(),
    password: z.string().min(1),
    ssl: z.boolean()
  }),
  redis: z.object({
    host: z.string(),
    port: z.number().int().positive(),
    password: z.string().optional(),
    tls: z.boolean()
  }),
  observability: z.object({
    otlpEndpoint: z.string().url(),
    metricsPort: z.number().int().positive()
  }),
  rateLimit: z.object({
    windowMs: z.number().int().positive(),
    maxRequests: z.number().int().positive()
  })
});

export type AppConfig = z.infer<typeof configSchema>;

export class ConfigManager {
  private static instance: AppConfig;

  static load(): AppConfig {
    if (ConfigManager.instance) {
      return ConfigManager.instance;
    }

    const rawConfig = {
      nodeEnv: process.env.NODE_ENV || 'development',
      port: parseInt(process.env.PORT || '3000'),
      logLevel: process.env.LOG_LEVEL || 'info',
      database: {
        host: process.env.DB_HOST || 'localhost',
        port: parseInt(process.env.DB_PORT || '5432'),
        name: process.env.DB_NAME || 'app',
        user: process.env.DB_USER || 'postgres',
        password: process.env.DB_PASSWORD || '',
        ssl: process.env.DB_SSL === 'true'
      },
      redis: {
        host: process.env.REDIS_HOST || 'localhost',
        port: parseInt(process.env.REDIS_PORT || '6379'),
        password: process.env.REDIS_PASSWORD,
        tls: process.env.REDIS_TLS === 'true'
      },
      observability: {
        otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318',
        metricsPort: parseInt(process.env.METRICS_PORT || '9090')
      },
      rateLimit: {
        windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '60000'),
        maxRequests: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100')
      }
    };

    try {
      ConfigManager.instance = configSchema.parse(rawConfig);
      return ConfigManager.instance;
    } catch (error) {
      if (error instanceof z.ZodError) {
        console.error('Configuration validation failed:', error.errors);
        throw new Error('Invalid configuration');
      }
      throw error;
    }
  }
}

Common Pitfalls and Edge Cases

Memory Leaks from Event Listeners: Forgetting to remove event listeners causes gradual memory accumulation. Always use removeListener or once for temporary subscriptions.

Blocking the Event Loop: Synchronous operations like JSON.parse on large payloads block the event loop. Use streaming parsers or worker threads for CPU-intensive tasks.

Improper Promise Handling: Unhandled promise rejections in async middleware crash applications. Always use try-catch in async functions or .catch() handlers.

Database Connection Exhaustion: Not releasing connections back to the pool causes connection starvation. Use connection pooling libraries and always close connections in finally blocks.

Timezone Inconsistencies: Storing dates without timezone information leads to data corruption. Always use UTC in databases and convert to local time in presentation layer.

Race Conditions in Distributed Systems: Multiple instances processing the same job cause duplicate operations. Implement distributed locks using Redis or database-level locking.

Insufficient Health Check Granularity: Simple HTTP 200 responses don't verify downstream dependencies. Implement deep health checks that verify database connectivity, cache availability, and external service reachability.

Production Deployment Checklist

  • Enable structured logging with correlation IDs across all services
  • Implement graceful shutdown handlers for all signal types
  • Configure connection pooling with appropriate limits for your traffic
  • Set up distributed tracing with OpenTelemetry
  • Implement circuit breakers for external service calls
  • Configure rate limiting at application and infrastructure levels
  • Enable automatic memory heap dumps on OOM conditions
  • Set up alerting for error rates, latency percentiles, and resource utilization
  • Implement health check endpoints with dependency verification
  • Configure log rotation and retention policies
  • Enable security headers (HSTS, CSP, X-Frame-Options)
  • Implement request timeout middleware
  • Set up automated secret rotation
  • Configure horizontal pod autoscaling based on custom metrics
  • Enable audit logging for sensitive operations

Frequently Asked Questions

What is the best way to handle uncaught exceptions in Node.js production?

Log the error with full context, trigger alerts, flush logs, and exit the process. Container orchestrators will restart the application. Never attempt to continue execution after uncaught exceptions as application state becomes unpredictable.

**How does graceful shutdown work in Kubernetes environments in