Part 4: Core Modules Essentials

fs, path, os, util, events, buffer, and Built-in Modules


Understanding Core Modules

Node.js includes dozens of built-in modules that don't require npm installation. These modules provide essential functionality for server-side applications: file system access, networking, cryptography, and more.
Core modules are compiled into Node.js binary and load faster than npm modules. When you require('fs'), Node.js doesn't search node_modules; it returns the built-in module immediately.

The fs Module: File System Operations

The fs module provides file system operations with three API styles:
  1. Callback-based (original)
  2. Synchronous (blocking)
  3. Promise-based (modern)
javascript
import { readFile, writeFile, mkdir, readdir, stat, unlink, rename } from 'fs/promises'; import { existsSync } from 'fs'; // Reading files async function readFileExample() { try { const content = await readFile('./config.json', 'utf8'); const config = JSON.parse(content); console.log(config); } catch (error) { if (error.code === 'ENOENT') { console.log('File not found'); } else { throw error; } } } // Writing files async function writeFileExample() { const data = { name: 'example', value: 42 }; await writeFile('./output.json', JSON.stringify(data, null, 2)); console.log('File written successfully'); } // Creating directories async function createDirectoryExample() { await mkdir('./logs/2024/01', { recursive: true }); // recursive: true creates parent directories if they don't exist } // Reading directory contents async function listFilesExample() { const files = await readdir('./src'); console.log(files); // ['index.js', 'utils.js', ...] // With file types const entries = await readdir('./src', { withFileTypes: true }); for (const entry of entries) { console.log(`${entry.name}: ${entry.isDirectory() ? 'directory' : 'file'}`); } } // File stats async function fileStatsExample() { const stats = await stat('./package.json'); console.log({ size: stats.size, isFile: stats.isFile(), isDirectory: stats.isDirectory(), created: stats.birthtime, modified: stats.mtime }); } // Deleting files async function deleteFileExample() { await unlink('./temp.txt'); } // Renaming/moving files async function renameFileExample() { await rename('./old-name.txt', './new-name.txt'); }

Callback-based API

Used when you need compatibility or specific patterns:
javascript
import { readFile, writeFile } from 'fs'; readFile('./config.json', 'utf8', (err, data) => { if (err) { console.error('Error reading file:', err.message); return; } console.log(data); }); // Callback pattern with error first is Node.js convention writeFile('./output.txt', 'Hello, World!', (err) => { if (err) throw err; console.log('File saved'); });

Synchronous API

Use sparingly, only during startup or in scripts:
javascript
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; // Check if file exists if (existsSync('./config.json')) { const config = JSON.parse(readFileSync('./config.json', 'utf8')); console.log(config); } // Create directory synchronously if (!existsSync('./logs')) { mkdirSync('./logs', { recursive: true }); } // Write synchronously writeFileSync('./output.txt', 'Hello, World!');

Watching Files

javascript
import { watch } from 'fs'; const watcher = watch('./config.json', (eventType, filename) => { console.log(`Event: ${eventType}, File: ${filename}`); }); // Clean up when done process.on('SIGINT', () => { watcher.close(); process.exit(0); });

File Handles for Fine-Grained Control

javascript
import { open } from 'fs/promises'; async function fileHandleExample() { const file = await open('./large-file.txt', 'r'); try { // Read specific bytes const buffer = Buffer.alloc(1024); const { bytesRead } = await file.read(buffer, 0, 1024, 0); console.log(`Read ${bytesRead} bytes`); // Get stats const stats = await file.stat(); console.log(`File size: ${stats.size}`); } finally { await file.close(); } }

The path Module: Path Manipulation

The path module handles file paths correctly across operating systems.
javascript
import path from 'path'; // Join path segments const filePath = path.join('users', 'john', 'documents', 'file.txt'); // On Windows: 'users\\john\\documents\\file.txt' // On Unix: 'users/john/documents/file.txt' // Resolve to absolute path const absolute = path.resolve('src', 'utils', 'helper.js'); // Returns absolute path from current working directory // Get directory name console.log(path.dirname('/home/user/file.txt')); // '/home/user' // Get filename console.log(path.basename('/home/user/file.txt')); // 'file.txt' console.log(path.basename('/home/user/file.txt', '.txt')); // 'file' // Get extension console.log(path.extname('/home/user/file.txt')); // '.txt' // Parse path into components const parsed = path.parse('/home/user/dir/file.txt'); console.log(parsed); // { // root: '/', // dir: '/home/user/dir', // base: 'file.txt', // ext: '.txt', // name: 'file' // } // Format components back to path const formatted = path.format({ dir: '/home/user', name: 'config', ext: '.json' }); console.log(formatted); // '/home/user/config.json' // Normalize path (resolve . and ..) console.log(path.normalize('/foo/bar//baz/asdf/quux/..')); // '/foo/bar/baz/asdf' // Check if path is absolute console.log(path.isAbsolute('/foo/bar')); // true console.log(path.isAbsolute('foo/bar')); // false // Get relative path console.log(path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb')); // '../../impl/bbb'

Common Pattern: Building Paths from Module Location

javascript
import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Load config relative to this file const configPath = path.join(__dirname, '..', 'config', 'settings.json'); // Resolve path relative to project root const projectRoot = path.resolve(__dirname, '..'); const dataPath = path.join(projectRoot, 'data', 'users.json');

The os Module: Operating System Information

javascript
import os from 'os'; // System information console.log('Platform:', os.platform()); // 'darwin', 'linux', 'win32' console.log('Architecture:', os.arch()); // 'x64', 'arm64' console.log('Release:', os.release()); // OS version console.log('Hostname:', os.hostname()); // Computer name console.log('Type:', os.type()); // 'Darwin', 'Linux', 'Windows_NT' // User information console.log('Home directory:', os.homedir()); // '/home/user' console.log('Temp directory:', os.tmpdir()); // '/tmp' console.log('User info:', os.userInfo()); // Memory console.log('Total memory:', os.totalmem() / 1024 / 1024 / 1024, 'GB'); console.log('Free memory:', os.freemem() / 1024 / 1024 / 1024, 'GB'); // CPUs const cpus = os.cpus(); console.log('CPU cores:', cpus.length); console.log('CPU model:', cpus[0].model); console.log('CPU speed:', cpus[0].speed, 'MHz'); // Network interfaces const networkInterfaces = os.networkInterfaces(); for (const [name, interfaces] of Object.entries(networkInterfaces)) { console.log(`\n${name}:`); interfaces.forEach(iface => { console.log(` ${iface.family}: ${iface.address}`); }); } // System uptime console.log('Uptime:', os.uptime() / 60 / 60, 'hours'); // Load average (Unix only) console.log('Load average:', os.loadavg()); // [1min, 5min, 15min] // End of line marker (platform-specific) const data = ['line1', 'line2', 'line3'].join(os.EOL);

Practical Example: System Health Check

javascript
import os from 'os'; function getSystemHealth() { const totalMemory = os.totalmem(); const freeMemory = os.freemem(); const usedMemory = totalMemory - freeMemory; const memoryUsagePercent = (usedMemory / totalMemory) * 100; const cpus = os.cpus(); const loadAverage = os.loadavg()[0]; // 1-minute average const cpuUsagePercent = (loadAverage / cpus.length) * 100; return { memory: { total: `${(totalMemory / 1024 / 1024 / 1024).toFixed(2)} GB`, used: `${(usedMemory / 1024 / 1024 / 1024).toFixed(2)} GB`, usagePercent: memoryUsagePercent.toFixed(2) }, cpu: { cores: cpus.length, model: cpus[0].model, loadAverage: loadAverage.toFixed(2), usagePercent: cpuUsagePercent.toFixed(2) }, uptime: `${(os.uptime() / 3600).toFixed(2)} hours`, platform: os.platform(), hostname: os.hostname() }; } console.log(getSystemHealth());

The util Module: Utility Functions

The util module provides useful utilities for debugging and working with Node.js.
javascript
import util from 'util'; // Promisify callback-based functions import { readFile as readFileCb } from 'fs'; const readFile = util.promisify(readFileCb); // Now we can use async/await const content = await readFile('./file.txt', 'utf8'); // Callbackify promise-based functions async function fetchData() { return { data: 'example' }; } const fetchDataCb = util.callbackify(fetchData); fetchDataCb((err, result) => { console.log(result); }); // Format strings console.log(util.format('%s has %d items', 'Cart', 5)); // 'Cart has 5 items' console.log(util.format('%j', { name: 'John' })); // '{"name":"John"}' // Inspect objects (for debugging) const complexObject = { name: 'test', nested: { deep: { value: [1, 2, 3], fn: () => {} } } }; console.log(util.inspect(complexObject, { depth: Infinity, colors: true, showHidden: true })); // Type checking console.log(util.types.isAsyncFunction(async () => {})); // true console.log(util.types.isPromise(Promise.resolve())); // true console.log(util.types.isDate(new Date())); // true // Deprecate functions const oldFunction = util.deprecate(() => { console.log('Do something'); }, 'oldFunction is deprecated, use newFunction instead'); oldFunction(); // Works but logs deprecation warning // TextEncoder/TextDecoder (also global) const encoder = new util.TextEncoder(); const decoder = new util.TextDecoder(); const encoded = encoder.encode('Hello'); console.log(encoded); // Uint8Array const decoded = decoder.decode(encoded); console.log(decoded); // 'Hello'

The events Module: EventEmitter

EventEmitter is the foundation of event-driven programming in Node.js.
javascript
import { EventEmitter } from 'events'; // Basic usage const emitter = new EventEmitter(); // Listen for events emitter.on('message', (data) => { console.log('Received:', data); }); // Emit events emitter.emit('message', { text: 'Hello!' }); // One-time listener emitter.once('connect', () => { console.log('Connected (only fires once)'); }); emitter.emit('connect'); // Fires emitter.emit('connect'); // Does nothing // Multiple listeners emitter.on('data', (d) => console.log('Listener 1:', d)); emitter.on('data', (d) => console.log('Listener 2:', d)); emitter.emit('data', 'test'); // Both listeners fire // Remove listeners function handler(data) { console.log(data); } emitter.on('event', handler); emitter.removeListener('event', handler); // or: emitter.off('event', handler); // Remove all listeners for an event emitter.removeAllListeners('event'); // Error handling - IMPORTANT // If no listener for 'error', it throws and crashes emitter.on('error', (err) => { console.error('Error:', err.message); }); emitter.emit('error', new Error('Something went wrong')); // Get listener count console.log(emitter.listenerCount('message')); // Async event iteration async function processEvents(emitter) { for await (const [data] of on(emitter, 'data')) { console.log('Data:', data); } }

Creating Custom Event Emitters

javascript
import { EventEmitter } from 'events'; class Database extends EventEmitter { constructor() { super(); this.connected = false; } connect() { // Simulate async connection setTimeout(() => { this.connected = true; this.emit('connected'); }, 100); } query(sql) { if (!this.connected) { this.emit('error', new Error('Not connected')); return; } this.emit('query', { sql, timestamp: Date.now() }); // Simulate query setTimeout(() => { this.emit('result', { sql, rows: [{ id: 1 }] }); }, 50); } close() { this.connected = false; this.emit('close'); } } const db = new Database(); db.on('connected', () => console.log('Database connected')); db.on('query', (data) => console.log('Executing:', data.sql)); db.on('result', (data) => console.log('Result:', data.rows)); db.on('error', (err) => console.error('DB Error:', err.message)); db.on('close', () => console.log('Database connection closed')); db.connect(); setTimeout(() => db.query('SELECT * FROM users'), 200); setTimeout(() => db.close(), 500);

Memory Leak Prevention

javascript
import { EventEmitter } from 'events'; const emitter = new EventEmitter(); // Default max listeners is 10 // Adding more logs a warning (potential memory leak) emitter.setMaxListeners(20); // Increase if needed // Or set globally EventEmitter.defaultMaxListeners = 20; // Check for memory leaks in tests function checkForLeaks(emitter, eventName) { const count = emitter.listenerCount(eventName); if (count > emitter.getMaxListeners()) { console.warn(`Possible memory leak: ${count} listeners for ${eventName}`); } }

The Buffer Class: Binary Data

Buffers handle binary data in Node.js when dealing with files, network protocols, or binary formats.
javascript
// Creating buffers const buf1 = Buffer.alloc(10); // 10 bytes, filled with zeros const buf2 = Buffer.alloc(10, 1); // 10 bytes, filled with 1s const buf3 = Buffer.allocUnsafe(10); // 10 bytes, uninitialized (faster) const buf4 = Buffer.from([1, 2, 3]); // From array const buf5 = Buffer.from('Hello', 'utf8'); // From string const buf6 = Buffer.from('48656c6c6f', 'hex'); // From hex // Buffer properties console.log(buf5.length); // 5 console.log(buf5.byteLength); // 5 // Reading from buffers console.log(buf5.toString()); // 'Hello' console.log(buf5.toString('hex')); // '48656c6c6f' console.log(buf5.toString('base64')); // 'SGVsbG8=' // Accessing bytes console.log(buf5[0]); // 72 (ASCII code for 'H') // Writing to buffers const buf = Buffer.alloc(6); buf.write('Hello'); buf[5] = 33; // '!' console.log(buf.toString()); // 'Hello!' // Writing numbers const numBuf = Buffer.alloc(4); numBuf.writeUInt32BE(12345678, 0); // Big-endian console.log(numBuf.readUInt32BE(0)); // 12345678 // Comparing buffers const a = Buffer.from('ABC'); const b = Buffer.from('ABC'); const c = Buffer.from('BCD'); console.log(a.equals(b)); // true console.log(Buffer.compare(a, c)); // -1 (a < c) // Copying buffers const source = Buffer.from('Hello'); const target = Buffer.alloc(10); source.copy(target, 0, 0, 5); console.log(target.toString()); // 'Hello\x00\x00\x00\x00\x00' // Slicing (creates a view, not a copy!) const original = Buffer.from('Hello World'); const slice = original.slice(0, 5); console.log(slice.toString()); // 'Hello' slice[0] = 74; // 'J' console.log(original.toString()); // 'Jello World' - original changed! // Concatenating buffers const combined = Buffer.concat([ Buffer.from('Hello '), Buffer.from('World') ]); console.log(combined.toString()); // 'Hello World' // Iterating over buffers for (const byte of buf5) { console.log(byte); // 72, 101, 108, 108, 111 }

Practical Example: Reading Binary File Headers

javascript
import { open } from 'fs/promises'; async function readPngHeader(filePath) { const file = await open(filePath, 'r'); try { const header = Buffer.alloc(24); await file.read(header, 0, 24, 0); // PNG files start with specific bytes const pngSignature = [137, 80, 78, 71, 13, 10, 26, 10]; const isPng = pngSignature.every((byte, i) => header[i] === byte); if (!isPng) { throw new Error('Not a PNG file'); } // Width and height are at offsets 16 and 20 (big-endian) const width = header.readUInt32BE(16); const height = header.readUInt32BE(20); return { width, height }; } finally { await file.close(); } }

The crypto Module: Cryptography

Node.js includes comprehensive cryptographic functionality.
javascript
import crypto from 'crypto'; // Hashing const hash = crypto.createHash('sha256'); hash.update('Hello, World!'); const digest = hash.digest('hex'); console.log(digest); // One-liner hash const quickHash = crypto.createHash('sha256') .update('password') .digest('hex'); // HMAC (Hash-based Message Authentication Code) const hmac = crypto.createHmac('sha256', 'secret-key'); hmac.update('message'); const hmacDigest = hmac.digest('hex'); // Random bytes const randomBytes = crypto.randomBytes(32); console.log(randomBytes.toString('hex')); // Random UUID const uuid = crypto.randomUUID(); console.log(uuid); // 'f47ac10b-58cc-4372-a567-0e02b2c3d479' // Password hashing with scrypt (recommended) async function hashPassword(password) { return new Promise((resolve, reject) => { const salt = crypto.randomBytes(16).toString('hex'); crypto.scrypt(password, salt, 64, (err, derivedKey) => { if (err) reject(err); resolve(`${salt}:${derivedKey.toString('hex')}`); }); }); } async function verifyPassword(password, hash) { return new Promise((resolve, reject) => { const [salt, key] = hash.split(':'); crypto.scrypt(password, salt, 64, (err, derivedKey) => { if (err) reject(err); resolve(crypto.timingSafeEqual( Buffer.from(key, 'hex'), derivedKey )); }); }); } // Symmetric encryption (AES) function encrypt(text, key) { const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); const authTag = cipher.getAuthTag(); return { iv: iv.toString('hex'), encrypted, authTag: authTag.toString('hex') }; } function decrypt(encrypted, key, iv, authTag) { const decipher = crypto.createDecipheriv( 'aes-256-gcm', key, Buffer.from(iv, 'hex') ); decipher.setAuthTag(Buffer.from(authTag, 'hex')); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } const key = crypto.randomBytes(32); // 256 bits const result = encrypt('Secret message', key); const original = decrypt(result.encrypted, key, result.iv, result.authTag); console.log(original); // 'Secret message'

Summary

Node.js core modules provide essential functionality:
ModulePurpose
fsFile system operations
pathCross-platform path handling
osOperating system information
utilUtility functions
eventsEvent-driven programming
bufferBinary data handling
cryptoCryptographic operations
Key practices:
  • Use fs/promises for async file operations
  • Always use path.join() for cross-platform paths
  • Handle EventEmitter 'error' events to prevent crashes
  • Use Buffers for binary data, not strings
  • Use built-in crypto for security-sensitive operations

Questions to Think About

  1. Why does Node.js have both sync and async file system functions?
Async operations don't block the event loop, which is critical for servers handling multiple requests. Sync operations are simpler and appropriate for startup scripts or CLIs where blocking is acceptable.
  1. When would you use Buffer instead of string?
Use Buffer for binary data (images, files, network packets), when you need precise byte control, or when dealing with encodings other than UTF-8. Strings are for text data only.
  1. How does EventEmitter enable loose coupling?
Components emit events without knowing who's listening. Listeners can be added or removed without modifying the emitter. This separation allows components to evolve independently.
  1. Why is path.join() better than string concatenation?
Path separators differ between OSes (/ vs ). path.join() handles this automatically and normalizes paths, preventing issues like double slashes.
  1. What happens if you don't handle EventEmitter errors?
The process crashes. EventEmitter throws unhandled 'error' events. Always add an error listener to prevent unexpected crashes.

Next: Part 5 - Asynchronous Programming, where we master callbacks, Promises, async/await, and error handling patterns.
All Blogs
Tags:nodejsfspathosbuffercore-modules