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
- Create a modular authentication system with separate modules for JWT, OAuth, and local authentication
- Build a plugin system that allows dynamic loading of modules
- Create an npm package for common utility functions
- 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