Core Modules — fs, path, os, and util

▶ Try It Yourself

Node.js ships with a rich standard library of built-in modules — no npm install required. These core modules cover file system operations, path manipulation, operating system information, and utility functions that you will use in almost every Node.js project. Understanding them well reduces your dependency on third-party packages, keeps your project lean, and gives you confidence working with the fundamental building blocks that frameworks like Express are built on top of. This lesson covers the four core modules you will use most frequently as a MEAN Stack backend developer.

Core Modules Overview

Module Import Primary Purpose
fs require('fs') or require('fs/promises') File system — read, write, watch, stat, mkdir, delete
path require('path') Cross-platform file path construction and parsing
os require('os') Operating system info — CPUs, memory, home directory, hostname
util require('util') Utilities — promisify, inspect, format, deprecate, types
crypto require('crypto') Hashing, encryption, random bytes, UUID
url require('url') URL parsing and construction (WHATWG URL API)
querystring require('querystring') Parse and stringify URL query strings (use URLSearchParams now)
child_process require('child_process') Spawn child processes, run shell commands

fs Module — Callback, Promise, and Sync APIs

Operation Callback API Promise API Sync API
Read file fs.readFile(path, cb) fs.promises.readFile(path) fs.readFileSync(path)
Write file fs.writeFile(path, data, cb) fs.promises.writeFile(path, data) fs.writeFileSync(path, data)
Append file fs.appendFile(path, data, cb) fs.promises.appendFile(path, data) fs.appendFileSync(path, data)
Check exists fs.promises.access(path) fs.existsSync(path)
Get info fs.stat(path, cb) fs.promises.stat(path) fs.statSync(path)
List directory fs.readdir(path, cb) fs.promises.readdir(path) fs.readdirSync(path)
Make directory fs.mkdir(path, cb) fs.promises.mkdir(path) fs.mkdirSync(path)
Delete file fs.unlink(path, cb) fs.promises.unlink(path) fs.unlinkSync(path)
Watch file fs.watch(path, listener)
Note: Always prefer fs.promises (the Promise-based API) over the callback-based API in modern Node.js code. It integrates naturally with async/await, produces cleaner error handling with try/catch, and avoids callback nesting. Only use the synchronous *Sync versions at application startup (before the server begins listening) where blocking is acceptable — never inside request handlers.
Tip: Use path.join() and path.resolve() everywhere — never string-concatenate file paths manually. On Windows, path separators are backslashes (\); on macOS and Linux they are forward slashes (/). path.join(__dirname, 'config', 'settings.json') produces the correct path on every platform automatically. String concatenation like __dirname + '/config/settings.json' breaks on Windows.
Warning: fs.existsSync(path) followed by fs.readFileSync(path) is a race condition — the file could be deleted between the two calls. Instead, just attempt the operation and handle the error: try { await fs.promises.readFile(path) } catch(e) { if (e.code === 'ENOENT') ... }. ENOENT means “Error NO ENTry” — the file or directory does not exist.

Basic Example

const fs   = require('fs/promises');   // Promise-based API (Node 14+)
const path = require('path');
const os   = require('os');
const util = require('util');

// ── path module ───────────────────────────────────────────────────────────
const configPath = path.join(__dirname, '..', 'config', 'settings.json');
console.log(configPath);    // /project/config/settings.json (any platform)

path.basename('/users/alice/resume.pdf');   // 'resume.pdf'
path.dirname('/users/alice/resume.pdf');    // '/users/alice'
path.extname('script.min.js');             // '.js'
path.parse('/users/alice/resume.pdf');
// { root: '/', dir: '/users/alice', base: 'resume.pdf', ext: '.pdf', name: 'resume' }

path.resolve('src', 'index.js');           // absolute path from cwd
path.relative('/app/src', '/app/tests');   // '../tests'
path.normalize('/app//src/../src/./');     // '/app/src/'

// ── fs module — async operations ─────────────────────────────────────────
async function fileOperations() {
    const filePath = path.join(__dirname, 'data.txt');

    // Write
    await fs.writeFile(filePath, 'Hello, Node.js!
Line 2
Line 3', 'utf8');

    // Read
    const content = await fs.readFile(filePath, 'utf8');
    console.log(content);

    // Append
    await fs.appendFile(filePath, '
Appended line');

    // Stat — metadata
    const stats = await fs.stat(filePath);
    console.log({
        size:       stats.size,             // bytes
        isFile:     stats.isFile(),
        isDirectory:stats.isDirectory(),
        created:    stats.birthtime,
        modified:   stats.mtime,
    });

    // List directory
    const entries = await fs.readdir(__dirname, { withFileTypes: true });
    entries.forEach(entry => {
        const type = entry.isDirectory() ? '[DIR]' : '[FILE]';
        console.log(type, entry.name);
    });

    // Make directory (recursive: true creates parent dirs too)
    await fs.mkdir(path.join(__dirname, 'uploads', '2025'), { recursive: true });

    // Delete
    await fs.unlink(filePath);
}

// ── fs.watch — watch for file changes ────────────────────────────────────
const watcher = require('fs').watch('./config', { recursive: true }, (event, filename) => {
    console.log(`[${event}] ${filename}`);
    // event: 'rename' (created/deleted) or 'change' (modified)
});
// watcher.close() to stop watching

// ── os module ─────────────────────────────────────────────────────────────
console.log(os.hostname());       // 'my-server'
console.log(os.platform());       // 'linux', 'darwin', 'win32'
console.log(os.arch());           // 'x64', 'arm64'
console.log(os.cpus().length);    // number of CPU cores
console.log(os.totalmem());       // total RAM in bytes
console.log(os.freemem());        // free RAM in bytes
console.log(os.homedir());        // '/home/alice' or 'C:/Users/Alice'
console.log(os.tmpdir());         // '/tmp' or 'C:/Temp'
console.log(os.uptime());         // system uptime in seconds
console.log(os.networkInterfaces()); // network interface details
console.log(os.EOL);              // '
' on Unix, '
' on Windows

// ── util module ───────────────────────────────────────────────────────────

// util.promisify — convert callback-based functions to Promise-based
const { promisify } = require('util');
const sleep = promisify(setTimeout);   // setTimeout(cb, delay) → Promise
await sleep(1000);                     // await a 1-second delay

// util.inspect — deep stringify any value (better than JSON.stringify)
const complex = { fn: () => 'hello', sym: Symbol('x'), undef: undefined };
console.log(util.inspect(complex, { depth: 4, colors: true }));
// { fn: [Function: fn], sym: Symbol(x), undef: undefined }
// JSON.stringify would silently drop fn, sym, and undef

// util.format — printf-style string formatting
util.format('Hello %s, you are %d years old', 'Alice', 30);
// 'Hello Alice, you are 30 years old'

// util.types — type checking utilities
util.types.isPromise(Promise.resolve());    // true
util.types.isAsyncFunction(async () => {}); // true
util.types.isDate(new Date());             // true
util.types.isRegExp(/abc/);               // true

// util.deprecate — mark a function as deprecated
const oldFunction = util.deprecate(
    () => 'old result',
    'oldFunction() is deprecated. Use newFunction() instead.'
);

Real-World Example: Config File Manager

// config-manager.js
const fs   = require('fs/promises');
const path = require('path');
const os   = require('os');

class ConfigManager {
    #configDir;
    #configPath;
    #config = {};
    #watcher = null;

    constructor(appName) {
        // Store config in OS-appropriate user config directory
        this.#configDir  = path.join(os.homedir(), '.config', appName);
        this.#configPath = path.join(this.#configDir, 'config.json');
    }

    async load() {
        await fs.mkdir(this.#configDir, { recursive: true });

        try {
            const raw      = await fs.readFile(this.#configPath, 'utf8');
            this.#config   = JSON.parse(raw);
        } catch (err) {
            if (err.code === 'ENOENT') {
                this.#config = {};          // no config file yet — start empty
            } else {
                throw new Error(`Failed to load config: ${err.message}`);
            }
        }
        return this;
    }

    async save() {
        const json = JSON.stringify(this.#config, null, 2);
        await fs.writeFile(this.#configPath, json, 'utf8');
        return this;
    }

    get(key, defaultValue = undefined) {
        return key.split('.').reduce((obj, k) => obj?.[k], this.#config) ?? defaultValue;
    }

    set(key, value) {
        const keys = key.split('.');
        let   obj  = this.#config;
        for (let i = 0; i < keys.length - 1; i++) {
            obj[keys[i]] = obj[keys[i]] ?? {};
            obj           = obj[keys[i]];
        }
        obj[keys.at(-1)] = value;
        return this;
    }

    watch(onChange) {
        this.#watcher = require('fs').watch(this.#configPath, async () => {
            await this.load();
            onChange(this.#config);
        });
    }

    stopWatching() {
        this.#watcher?.close();
    }
}

// Usage
const config = new ConfigManager('task-manager');
await config.load();
config.set('server.port', 3000).set('server.host', 'localhost');
await config.save();
console.log(config.get('server.port'));     // 3000
console.log(config.get('db.uri', 'mongodb://localhost:27017'));

Common Mistakes

Mistake 1 — Building paths with string concatenation

❌ Wrong — breaks on Windows where separator is backslash:

const filePath = __dirname + '/config/' + filename;   // fails on Windows

✅ Correct — always use path.join:

const filePath = path.join(__dirname, 'config', filename);   // works everywhere

Mistake 2 — Using existsSync before read — race condition

❌ Wrong — file could be deleted between the two calls:

if (fs.existsSync(filePath)) {
    const data = fs.readFileSync(filePath);   // race condition — may throw!
}

✅ Correct — attempt and handle the error directly:

try {
    const data = await fs.promises.readFile(filePath, 'utf8');
} catch (err) {
    if (err.code !== 'ENOENT') throw err;   // only ignore "not found"
}

Mistake 3 — Forgetting that os.freemem() returns bytes, not MB

❌ Wrong — logs raw bytes, misleadingly large number:

console.log(`Free memory: ${os.freemem()} MB`);   // "Free memory: 4294967296 MB" ← wrong

✅ Correct — convert to the desired unit:

const freeMB = (os.freemem() / 1024 / 1024).toFixed(0);
console.log(`Free memory: ${freeMB} MB`);   // "Free memory: 4096 MB"

Quick Reference

Task Code
Read file (async) await fs.promises.readFile(path, 'utf8')
Write file (async) await fs.promises.writeFile(path, data, 'utf8')
Make directory await fs.promises.mkdir(path, { recursive: true })
File exists check try { await fs.promises.access(path) } catch { /* not found */ }
Join paths path.join(__dirname, 'folder', 'file.json')
Absolute path path.resolve('relative/path')
File extension path.extname('file.min.js')'.js'
CPU count os.cpus().length
Free memory (MB) (os.freemem() / 1024 / 1024).toFixed(0)
Promisify callback fn util.promisify(callbackFn)
Deep inspect value util.inspect(value, { depth: 4, colors: true })

🧠 Test Yourself

You need to read a JSON config file at startup. Which approach is correct?





▶ Try It Yourself