Skip to content

Components and Modules

Overview

Node.js applications are built using modules - reusable pieces of code that encapsulate functionality. This chapter covers how to create, organize, and use modules effectively, including CommonJS modules, ES6 modules, and npm packages.

Module Systems

CommonJS Modules (Default)

CommonJS is the traditional module system in Node.js:

javascript
// math-utils.js - Exporting functions
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

function multiply(a, b) {
  return a * b;
}

function divide(a, b) {
  if (b === 0) {
    throw new Error('Division by zero');
  }
  return a / b;
}

// Different export patterns
module.exports = {
  add,
  subtract,
  multiply,
  divide
};

// Alternative: individual exports
// exports.add = add;
// exports.subtract = subtract;
javascript
// calculator.js - Exporting a class
class Calculator {
  constructor() {
    this.history = [];
    this.memory = 0;
  }

  calculate(operation, a, b) {
    let result;
    
    switch (operation) {
      case 'add':
        result = a + b;
        break;
      case 'subtract':
        result = a - b;
        break;
      case 'multiply':
        result = a * b;
        break;
      case 'divide':
        if (b === 0) throw new Error('Division by zero');
        result = a / b;
        break;
      default:
        throw new Error('Unknown operation');
    }

    this.history.push({ operation, a, b, result, timestamp: new Date() });
    return result;
  }

  getHistory() {
    return [...this.history];
  }

  clearHistory() {
    this.history = [];
  }

  memoryStore(value) {
    this.memory = value;
  }

  memoryRecall() {
    return this.memory;
  }

  memoryClear() {
    this.memory = 0;
  }
}

module.exports = Calculator;
javascript
// app.js - Using modules
const mathUtils = require('./math-utils');
const Calculator = require('./calculator');

// Using function exports
console.log('Addition:', mathUtils.add(5, 3));
console.log('Subtraction:', mathUtils.subtract(10, 4));

// Using class export
const calc = new Calculator();
console.log('Calculator result:', calc.calculate('multiply', 6, 7));
console.log('History:', calc.getHistory());

ES6 Modules (ESM)

Enable ES6 modules by adding "type": "module" to package.json:

javascript
// math-utils.mjs - Named exports
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

export const PI = 3.14159;

export class MathHelper {
  static square(n) {
    return n * n;
  }

  static cube(n) {
    return n * n * n;
  }
}

// Default export
export default function multiply(a, b) {
  return a * b;
}
javascript
// calculator.mjs - Default and named exports
export default class Calculator {
  constructor() {
    this.value = 0;
  }

  add(n) {
    this.value += n;
    return this;
  }

  subtract(n) {
    this.value -= n;
    return this;
  }

  multiply(n) {
    this.value *= n;
    return this;
  }

  divide(n) {
    if (n === 0) throw new Error('Division by zero');
    this.value /= n;
    return this;
  }

  getValue() {
    return this.value;
  }

  reset() {
    this.value = 0;
    return this;
  }
}

export const CONSTANTS = {
  PI: 3.14159,
  E: 2.71828
};

export function formatResult(value, decimals = 2) {
  return parseFloat(value.toFixed(decimals));
}
javascript
// app.mjs - Using ES6 modules
import multiply, { add, subtract, PI, MathHelper } from './math-utils.mjs';
import Calculator, { CONSTANTS, formatResult } from './calculator.mjs';

// Using named imports
console.log('Addition:', add(5, 3));
console.log('PI value:', PI);
console.log('Square:', MathHelper.square(4));

// Using default import
console.log('Multiplication:', multiply(6, 7));

// Using class
const calc = new Calculator();
const result = calc.add(10).multiply(2).subtract(5).getValue();
console.log('Chained calculation:', formatResult(result));

Creating Reusable Components

Database Connection Module

javascript
// database/connection.js
const mongoose = require('mongoose');
const config = require('../config');

class DatabaseConnection {
  constructor() {
    this.connection = null;
    this.isConnected = false;
  }

  async connect() {
    try {
      if (this.isConnected) {
        return this.connection;
      }

      this.connection = await mongoose.connect(config.database.url, {
        useNewUrlParser: true,
        useUnifiedTopology: true,
        maxPoolSize: config.database.poolSize
      });

      this.isConnected = true;
      console.log('Database connected successfully');

      // Handle connection events
      mongoose.connection.on('error', (error) => {
        console.error('Database connection error:', error);
        this.isConnected = false;
      });

      mongoose.connection.on('disconnected', () => {
        console.log('Database disconnected');
        this.isConnected = false;
      });

      return this.connection;
    } catch (error) {
      console.error('Database connection failed:', error);
      throw error;
    }
  }

  async disconnect() {
    if (this.connection) {
      await mongoose.disconnect();
      this.isConnected = false;
      console.log('Database disconnected');
    }
  }

  getConnection() {
    return this.connection;
  }

  isConnectionActive() {
    return this.isConnected;
  }
}

// Singleton pattern
const dbConnection = new DatabaseConnection();
module.exports = dbConnection;

Logger Module

javascript
// utils/logger.js
const winston = require('winston');
const path = require('path');
const config = require('../config');

class Logger {
  constructor() {
    this.logger = this.createLogger();
  }

  createLogger() {
    const logFormat = winston.format.combine(
      winston.format.timestamp(),
      winston.format.errors({ stack: true }),
      winston.format.json()
    );

    const transports = [
      new winston.transports.File({
        filename: path.join('logs', 'error.log'),
        level: 'error',
        maxsize: 5242880, // 5MB
        maxFiles: 5
      }),
      new winston.transports.File({
        filename: path.join('logs', 'combined.log'),
        maxsize: 5242880,
        maxFiles: 5
      })
    ];

    // Add console transport in development
    if (config.app.env !== 'production') {
      transports.push(
        new winston.transports.Console({
          format: winston.format.combine(
            winston.format.colorize(),
            winston.format.simple()
          )
        })
      );
    }

    return winston.createLogger({
      level: config.logging.level,
      format: logFormat,
      transports
    });
  }

  info(message, meta = {}) {
    this.logger.info(message, meta);
  }

  error(message, meta = {}) {
    this.logger.error(message, meta);
  }

  warn(message, meta = {}) {
    this.logger.warn(message, meta);
  }

  debug(message, meta = {}) {
    this.logger.debug(message, meta);
  }

  // HTTP request logging
  logRequest(req, res, responseTime) {
    const logData = {
      method: req.method,
      url: req.url,
      statusCode: res.statusCode,
      responseTime: `${responseTime}ms`,
      userAgent: req.get('User-Agent'),
      ip: req.ip
    };

    if (res.statusCode >= 400) {
      this.error('HTTP Request Error', logData);
    } else {
      this.info('HTTP Request', logData);
    }
  }

  // Database operation logging
  logDatabaseOperation(operation, collection, duration, error = null) {
    const logData = {
      operation,
      collection,
      duration: `${duration}ms`
    };

    if (error) {
      this.error('Database Operation Failed', { ...logData, error: error.message });
    } else {
      this.debug('Database Operation', logData);
    }
  }
}

// Singleton pattern
const logger = new Logger();
module.exports = logger;

Email Service Module

javascript
// services/email-service.js
const nodemailer = require('nodemailer');
const fs = require('fs').promises;
const path = require('path');
const config = require('../config');
const logger = require('../utils/logger');

class EmailService {
  constructor() {
    this.transporter = null;
    this.templates = new Map();
    this.init();
  }

  async init() {
    try {
      // Create transporter
      this.transporter = nodemailer.createTransporter({
        host: config.email.smtp.host,
        port: config.email.smtp.port,
        secure: config.email.smtp.secure,
        auth: config.email.smtp.auth
      });

      // Verify connection
      await this.transporter.verify();
      logger.info('Email service initialized successfully');

      // Load email templates
      await this.loadTemplates();
    } catch (error) {
      logger.error('Email service initialization failed', { error: error.message });
      throw error;
    }
  }

  async loadTemplates() {
    const templatesDir = path.join(__dirname, '../templates/email');
    
    try {
      const templateFiles = await fs.readdir(templatesDir);
      
      for (const file of templateFiles) {
        if (file.endsWith('.html')) {
          const templateName = path.basename(file, '.html');
          const templatePath = path.join(templatesDir, file);
          const templateContent = await fs.readFile(templatePath, 'utf8');
          this.templates.set(templateName, templateContent);
        }
      }
      
      logger.info(`Loaded ${this.templates.size} email templates`);
    } catch (error) {
      logger.warn('Failed to load email templates', { error: error.message });
    }
  }

  async sendEmail(to, subject, template, data = {}) {
    try {
      let html = this.templates.get(template);
      
      if (!html) {
        throw new Error(`Template '${template}' not found`);
      }

      // Simple template variable replacement
      html = this.replaceTemplateVariables(html, data);

      const mailOptions = {
        from: `${config.email.from.name} <${config.email.from.address}>`,
        to,
        subject,
        html
      };

      const result = await this.transporter.sendMail(mailOptions);
      
      logger.info('Email sent successfully', {
        to,
        subject,
        template,
        messageId: result.messageId
      });

      return result;
    } catch (error) {
      logger.error('Failed to send email', {
        to,
        subject,
        template,
        error: error.message
      });
      throw error;
    }
  }

  replaceTemplateVariables(template, data) {
    let result = template;
    
    for (const [key, value] of Object.entries(data)) {
      const regex = new RegExp(`{{${key}}}`, 'g');
      result = result.replace(regex, value);
    }
    
    return result;
  }

  async sendWelcomeEmail(userEmail, userName) {
    return this.sendEmail(
      userEmail,
      'Welcome to Our Platform!',
      'welcome',
      {
        userName,
        loginUrl: `${config.app.url}/login`,
        supportEmail: config.email.from.address
      }
    );
  }

  async sendPasswordResetEmail(userEmail, resetToken) {
    const resetUrl = `${config.app.url}/reset-password?token=${resetToken}`;
    
    return this.sendEmail(
      userEmail,
      'Password Reset Request',
      'reset-password',
      {
        resetUrl,
        expiryTime: '1 hour'
      }
    );
  }
}

module.exports = EmailService;

Cache Module

javascript
// services/cache-service.js
const redis = require('redis');
const config = require('../config');
const logger = require('../utils/logger');

class CacheService {
  constructor() {
    this.client = null;
    this.isConnected = false;
    this.defaultTTL = config.redis.ttl;
  }

  async connect() {
    try {
      this.client = redis.createClient({
        host: config.redis.host,
        port: config.redis.port,
        password: config.redis.password,
        db: config.redis.db
      });

      this.client.on('error', (error) => {
        logger.error('Redis connection error', { error: error.message });
        this.isConnected = false;
      });

      this.client.on('connect', () => {
        logger.info('Redis connected');
        this.isConnected = true;
      });

      this.client.on('disconnect', () => {
        logger.warn('Redis disconnected');
        this.isConnected = false;
      });

      await this.client.connect();
      return this.client;
    } catch (error) {
      logger.error('Failed to connect to Redis', { error: error.message });
      throw error;
    }
  }

  async disconnect() {
    if (this.client) {
      await this.client.disconnect();
      this.isConnected = false;
    }
  }

  generateKey(prefix, identifier) {
    return `${config.redis.keyPrefix}${prefix}:${identifier}`;
  }

  async get(key) {
    try {
      if (!this.isConnected) {
        logger.warn('Cache not available, skipping get operation');
        return null;
      }

      const value = await this.client.get(key);
      
      if (value) {
        logger.debug('Cache hit', { key });
        return JSON.parse(value);
      }
      
      logger.debug('Cache miss', { key });
      return null;
    } catch (error) {
      logger.error('Cache get error', { key, error: error.message });
      return null;
    }
  }

  async set(key, value, ttl = this.defaultTTL) {
    try {
      if (!this.isConnected) {
        logger.warn('Cache not available, skipping set operation');
        return false;
      }

      const serializedValue = JSON.stringify(value);
      await this.client.setEx(key, ttl, serializedValue);
      
      logger.debug('Cache set', { key, ttl });
      return true;
    } catch (error) {
      logger.error('Cache set error', { key, error: error.message });
      return false;
    }
  }

  async delete(key) {
    try {
      if (!this.isConnected) {
        return false;
      }

      const result = await this.client.del(key);
      logger.debug('Cache delete', { key, deleted: result > 0 });
      return result > 0;
    } catch (error) {
      logger.error('Cache delete error', { key, error: error.message });
      return false;
    }
  }

  async exists(key) {
    try {
      if (!this.isConnected) {
        return false;
      }

      const result = await this.client.exists(key);
      return result === 1;
    } catch (error) {
      logger.error('Cache exists error', { key, error: error.message });
      return false;
    }
  }

  async flush() {
    try {
      if (!this.isConnected) {
        return false;
      }

      await this.client.flushDb();
      logger.info('Cache flushed');
      return true;
    } catch (error) {
      logger.error('Cache flush error', { error: error.message });
      return false;
    }
  }

  // Higher-level caching methods
  async cacheFunction(key, fn, ttl = this.defaultTTL) {
    const cachedResult = await this.get(key);
    
    if (cachedResult !== null) {
      return cachedResult;
    }

    const result = await fn();
    await this.set(key, result, ttl);
    return result;
  }

  async invalidatePattern(pattern) {
    try {
      if (!this.isConnected) {
        return false;
      }

      const keys = await this.client.keys(pattern);
      
      if (keys.length > 0) {
        await this.client.del(keys);
        logger.info('Cache pattern invalidated', { pattern, keysDeleted: keys.length });
      }
      
      return true;
    } catch (error) {
      logger.error('Cache pattern invalidation error', { pattern, error: error.message });
      return false;
    }
  }
}

module.exports = CacheService;

Module Organization Patterns

Repository Pattern

javascript
// repositories/base-repository.js
class BaseRepository {
  constructor(model) {
    this.model = model;
  }

  async findById(id) {
    return await this.model.findById(id);
  }

  async findAll(options = {}) {
    const { page = 1, limit = 10, sort = { createdAt: -1 } } = options;
    const skip = (page - 1) * limit;

    const [items, total] = await Promise.all([
      this.model.find().sort(sort).skip(skip).limit(limit),
      this.model.countDocuments()
    ]);

    return {
      items,
      total,
      page,
      limit,
      pages: Math.ceil(total / limit)
    };
  }

  async create(data) {
    const item = new this.model(data);
    return await item.save();
  }

  async update(id, data) {
    return await this.model.findByIdAndUpdate(id, data, { new: true });
  }

  async delete(id) {
    return await this.model.findByIdAndDelete(id);
  }

  async findByField(field, value) {
    return await this.model.find({ [field]: value });
  }

  async exists(id) {
    const count = await this.model.countDocuments({ _id: id });
    return count > 0;
  }
}

module.exports = BaseRepository;
javascript
// repositories/user-repository.js
const BaseRepository = require('./base-repository');
const User = require('../models/User');

class UserRepository extends BaseRepository {
  constructor() {
    super(User);
  }

  async findByEmail(email) {
    return await this.model.findOne({ email });
  }

  async findActiveUsers() {
    return await this.model.find({ isActive: true });
  }

  async updateLastLogin(userId) {
    return await this.model.findByIdAndUpdate(
      userId,
      { lastLoginAt: new Date() },
      { new: true }
    );
  }

  async searchUsers(query, options = {}) {
    const { page = 1, limit = 10 } = options;
    const skip = (page - 1) * limit;

    const searchRegex = new RegExp(query, 'i');
    const filter = {
      $or: [
        { name: searchRegex },
        { email: searchRegex }
      ]
    };

    const [users, total] = await Promise.all([
      this.model.find(filter).skip(skip).limit(limit),
      this.model.countDocuments(filter)
    ]);

    return {
      users,
      total,
      page,
      limit,
      pages: Math.ceil(total / limit)
    };
  }
}

module.exports = UserRepository;

Service Layer Pattern

javascript
// services/user-service.js
const UserRepository = require('../repositories/user-repository');
const EmailService = require('./email-service');
const CacheService = require('./cache-service');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const config = require('../config');
const logger = require('../utils/logger');

class UserService {
  constructor() {
    this.userRepository = new UserRepository();
    this.emailService = new EmailService();
    this.cacheService = new CacheService();
  }

  async createUser(userData) {
    try {
      // Check if user already exists
      const existingUser = await this.userRepository.findByEmail(userData.email);
      if (existingUser) {
        throw new Error('User with this email already exists');
      }

      // Hash password
      const hashedPassword = await bcrypt.hash(userData.password, config.security.bcryptRounds);
      
      // Create user
      const user = await this.userRepository.create({
        ...userData,
        password: hashedPassword,
        isActive: true,
        createdAt: new Date()
      });

      // Send welcome email
      await this.emailService.sendWelcomeEmail(user.email, user.name);

      // Cache user data
      const cacheKey = this.cacheService.generateKey('user', user._id);
      await this.cacheService.set(cacheKey, this.sanitizeUser(user));

      logger.info('User created successfully', { userId: user._id, email: user.email });
      
      return this.sanitizeUser(user);
    } catch (error) {
      logger.error('Failed to create user', { error: error.message, userData: { email: userData.email } });
      throw error;
    }
  }

  async getUserById(userId) {
    try {
      // Try cache first
      const cacheKey = this.cacheService.generateKey('user', userId);
      const cachedUser = await this.cacheService.get(cacheKey);
      
      if (cachedUser) {
        return cachedUser;
      }

      // Fetch from database
      const user = await this.userRepository.findById(userId);
      
      if (!user) {
        return null;
      }

      const sanitizedUser = this.sanitizeUser(user);
      
      // Cache the result
      await this.cacheService.set(cacheKey, sanitizedUser);
      
      return sanitizedUser;
    } catch (error) {
      logger.error('Failed to get user by ID', { userId, error: error.message });
      throw error;
    }
  }

  async authenticateUser(email, password) {
    try {
      const user = await this.userRepository.findByEmail(email);
      
      if (!user) {
        throw new Error('Invalid credentials');
      }

      const isPasswordValid = await bcrypt.compare(password, user.password);
      
      if (!isPasswordValid) {
        throw new Error('Invalid credentials');
      }

      if (!user.isActive) {
        throw new Error('Account is deactivated');
      }

      // Update last login
      await this.userRepository.updateLastLogin(user._id);

      // Generate JWT token
      const token = jwt.sign(
        { userId: user._id, email: user.email },
        config.jwt.secret,
        { expiresIn: config.jwt.expiresIn }
      );

      logger.info('User authenticated successfully', { userId: user._id, email });

      return {
        user: this.sanitizeUser(user),
        token
      };
    } catch (error) {
      logger.error('Authentication failed', { email, error: error.message });
      throw error;
    }
  }

  async updateUser(userId, updateData) {
    try {
      // Remove sensitive fields
      const { password, ...safeUpdateData } = updateData;
      
      const updatedUser = await this.userRepository.update(userId, safeUpdateData);
      
      if (!updatedUser) {
        throw new Error('User not found');
      }

      // Invalidate cache
      const cacheKey = this.cacheService.generateKey('user', userId);
      await this.cacheService.delete(cacheKey);

      logger.info('User updated successfully', { userId });
      
      return this.sanitizeUser(updatedUser);
    } catch (error) {
      logger.error('Failed to update user', { userId, error: error.message });
      throw error;
    }
  }

  async searchUsers(query, options) {
    try {
      const result = await this.userRepository.searchUsers(query, options);
      
      return {
        ...result,
        users: result.users.map(user => this.sanitizeUser(user))
      };
    } catch (error) {
      logger.error('Failed to search users', { query, error: error.message });
      throw error;
    }
  }

  sanitizeUser(user) {
    const { password, ...sanitizedUser } = user.toObject ? user.toObject() : user;
    return sanitizedUser;
  }
}

module.exports = UserService;

NPM Package Creation

Creating a Reusable Package

javascript
// package.json for a utility package
{
  "name": "@mycompany/node-utils",
  "version": "1.0.0",
  "description": "Utility functions for Node.js applications",
  "main": "index.js",
  "module": "index.mjs",
  "types": "index.d.ts",
  "files": [
    "lib/",
    "index.js",
    "index.mjs",
    "index.d.ts",
    "README.md"
  ],
  "scripts": {
    "build": "babel src --out-dir lib",
    "test": "jest",
    "lint": "eslint src/",
    "prepublishOnly": "npm run build && npm test"
  },
  "keywords": ["nodejs", "utilities", "helpers"],
  "author": "Your Name",
  "license": "MIT",
  "dependencies": {},
  "devDependencies": {
    "@babel/cli": "^7.0.0",
    "@babel/core": "^7.0.0",
    "@babel/preset-env": "^7.0.0",
    "jest": "^29.0.0",
    "eslint": "^8.0.0"
  }
}
javascript
// index.js - Main entry point
const { formatDate, formatCurrency } = require('./lib/formatters');
const { validateEmail, validatePhone } = require('./lib/validators');
const { generateId, generateSlug } = require('./lib/generators');
const { asyncRetry, asyncTimeout } = require('./lib/async-utils');

module.exports = {
  formatters: {
    formatDate,
    formatCurrency
  },
  validators: {
    validateEmail,
    validatePhone
  },
  generators: {
    generateId,
    generateSlug
  },
  asyncUtils: {
    asyncRetry,
    asyncTimeout
  }
};

Next Steps

In the next chapter, we'll explore routing and navigation in Node.js applications, including Express.js routing patterns.

Practice Exercises

  1. Create a modular authentication system with separate modules for JWT, OAuth, and local authentication
  2. Build a plugin system that allows dynamic loading of modules
  3. Create an npm package for common utility functions
  4. Implement a service layer with dependency injection

Key Takeaways

  • Modules are the building blocks of Node.js applications
  • CommonJS and ES6 modules provide different approaches to code organization
  • Repository pattern separates data access logic
  • Service layer encapsulates business logic
  • Proper module organization improves maintainability and testability
  • Creating reusable packages promotes code sharing across projects
  • Dependency injection makes modules more testable and flexible

Content is for learning and research only.