Skip to content
Node.js Core Modules — File System, HTTP, Streams & EventEmitter

Node.js Core Modules — File System, HTTP, Streams & EventEmitter

DodaTech Updated Jun 6, 2026 7 min read

Node.js ships with over 30 built-in modules that provide everything needed for servers, file operations, binary data, OS interaction, and cryptography — without installing any third-party packages.

What You’ll Learn

By the end of this tutorial, you’ll create an HTTP server, read and write files using streams, use EventEmitter for custom events, work with buffers and crypto, and build a CLI file sync tool.

Why Core Modules Matter

Node.js core modules are the foundation everything else builds on. Durga Antivirus Pro uses the crypto module for hash-based threat detection and fs for file scanning. Doda Browser uses http and events for its sync service. DodaZIP uses stream and zlib for archive processing. Understanding core modules means you can build sophisticated tools without external dependencies.

Core Modules Learning Path

    flowchart TD
  A[Async JS] --> B[Core Modules]
  B --> C[Express.js]
  B --> D[Database]
  B --> E[Advanced]
  B --> F{You Are Here}
  style F fill:#f90,color:#fff
  
Prerequisites: https://tutorials.dodatech.com/backend/nodejs/nodejs-basics/ and https://tutorials.dodatech.com/backend/nodejs/nodejs-async/. You should be comfortable with JavaScript callbacks, promises, and async/await.

HTTP Module — Build a Server From Scratch

const http = require("node:http");

const server = http.createServer((req, res) => {
  console.log(`${req.method} ${req.url}`);
  res.writeHead(200, { "Content-Type": "application/json" });
  res.end(JSON.stringify({ message: "Hello World", url: req.url }));
});

server.listen(3000, () => {
  console.log("Server at http://localhost:3000");
});

Line by line:

  1. require("node:http") — import the HTTP module (note the node: prefix — it’s a Node.js core protocol)
  2. http.createServer(callback) — creates a server; the callback runs on every request
  3. req — the request object (method, url, headers)
  4. res — the response object (writeHead, end)
  5. server.listen(3000) — bind to port 3000

File System (fs) — Read and Write Files

const fs = require("node:fs/promises");  // Promise-based API

// Reading
const data = await fs.readFile("file.txt", "utf8");

// Writing
await fs.writeFile("output.txt", "Hello World", "utf8");

// Appending
await fs.appendFile("log.txt", "New log entry\n");

// Directory operations
await fs.mkdir("new-dir", { recursive: true });
const files = await fs.readdir(".");
await fs.rm("file.txt");

// File info
const stats = await fs.stat("file.txt");
console.log(stats.size, stats.isFile(), stats.mtime);

Always use the fs/promises API in modern Node.js. Avoid fs.readFileSync in server code — it blocks the event loop.

Path Module — File Path Manipulation

const path = require("node:path");

console.log(path.join("users", "alice", "docs"));     // users/alice/docs
console.log(path.resolve("app", "..", "config"));     // /home/user/config
console.log(path.basename("/usr/bin/node"));           // node
console.log(path.dirname("/usr/bin/node"));            // /usr/bin
console.log(path.extname("image.jpg"));                // .jpg

path.join() builds paths correctly across operating systems. path.resolve() gives absolute paths. Never concatenate paths with string templates — use these functions.

Events (EventEmitter) — Publish/Subscribe Pattern

const EventEmitter = require("node:events");

class OrderSystem extends EventEmitter {}

const orders = new OrderSystem();

orders.on("order-placed", (order) => {
  console.log(`Order #${order.id} for ${order.customer}`);
});

orders.emit("order-placed", { id: 1001, customer: "Alice" });

// Once — fire only on first event
orders.once("startup", () => console.log("First run only"));

EventEmitter follows the publish/subscribe pattern — objects emit named events and attached listeners react. Core Node.js modules use it extensively (HTTP requests, streams, process).

Streams — Process Data Chunk by Chunk

const fs = require("node:fs");
const zlib = require("node:zlib");
const { pipeline } = require("node:stream/promises");

// Read a large file in chunks (doesn't load entire file into memory)
const readStream = fs.createReadStream("large-file.txt", "utf8");
readStream.on("data", (chunk) => console.log("Chunk:", chunk.length));
readStream.on("end", () => console.log("Done"));

// Pipe — connect readable to writable
const read = fs.createReadStream("source.txt");
const write = fs.createWriteStream("dest.txt");
read.pipe(write);

// Compress with pipeline (Node 15+)
await pipeline(
  fs.createReadStream("file.txt"),
  zlib.createGzip(),
  fs.createWriteStream("file.txt.gz")
);

Streams are essential for large files — they process data in chunks instead of loading everything into memory. Use pipeline() for proper error handling and cleanup.

Buffer & Crypto

const crypto = require("node:crypto");

// SHA-256 hash
const hash = crypto.createHash("sha256");
hash.update("password123");
console.log(hash.digest("hex"));

// Random bytes
const bytes = crypto.randomBytes(16);
console.log(bytes.toString("hex"));

// Generate UUID
const uuid = crypto.randomUUID();

Common Mistakes

1. Using sync fs methods in a serverfs.readFileSync blocks the entire event loop. Always use fs/promises.

2. Not handling stream errors — Streams emit error events. If unhandled, they crash the process. Use .on('error', handler) or pipeline().

3. Modifying a subarray buffer.subarray() doesn’t copy data — modifying the slice also modifies the original. Use Buffer.from(slice) for a copy.

4. Forgetting res.end() — HTTP responses hang until res.end() is called. Ensure every code path calls it.

5. Exceeding max listeners — Over 10 listeners on an EventEmitter prints a warning. Use emitter.setMaxListeners(n).

Common Patterns

Configuration Loader Using Core Modules

A common real-world task: load JSON config with environment variable overrides, using only built-in modules:

const fs = require("node:fs/promises");
const path = require("node:path");

async function loadConfig(filePath) {
  const absPath = path.resolve(filePath);
  const raw = await fs.readFile(absPath, "utf8");
  const config = JSON.parse(raw);

  // Environment variable overrides
  for (const [key, value] of Object.entries(process.env)) {
    if (key.startsWith("APP_")) {
      const configKey = key.slice(4).toLowerCase();
      config[configKey] = value;
    }
  }
  return config;
}

// Usage
// config.json: { "port": 3000, "db_host": "localhost" }
// env: APP_PORT=4000
// Result: { "port": "4000", "db_host": "localhost" }
loadConfig("./config.json").then(console.log);

Expected output (with APP_PORT=4000):

{ port: "4000", db_host: "localhost" }

Why it matters: This pattern is used by Doda Browser to merge config file defaults with environment-specific overrides during deployment — no third-party libraries needed.

CLI Watcher Using fs.watch

Monitor a directory for file changes and react using core modules:

const fs = require("node:fs");
const path = require("node:path");
const { EventEmitter } = require("node:events");

class FileWatcher extends EventEmitter {
  constructor(dir) {
    super();
    this.dir = dir;
  }

  start() {
    fs.watch(this.dir, (eventType, filename) => {
      const fullPath = path.join(this.dir, filename);
      this.emit("change", { eventType, filename, fullPath });
    });
  }
}

const watcher = new FileWatcher("./uploads");
watcher.on("change", ({ eventType, filename }) => {
  console.log(`[${eventType}] ${filename} was modified`);
});
watcher.start();

Expected output when a file is added:

[rename] report.pdf was modified

Real-world use: Durga Antivirus Pro uses fs.watch to detect new files dropped in quarantine folders, triggering an automatic scan without polling.

Retry Logic with EventEmitter

Network operations fail. A retry pattern using events makes the failure handling transparent:

const { EventEmitter } = require("node:events");

async function fetchWithRetry(url, maxRetries = 3) {
  const emitter = new EventEmitter();
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const res = await fetch(url);
      emitter.emit("success", { attempt, status: res.status });
      return res;
    } catch (err) {
      emitter.emit("retry", { attempt, error: err.message });
      if (attempt === maxRetries) throw err;
      await new Promise(r => setTimeout(r, 1000 * attempt)); // backoff
    }
  }
}

const result = fetchWithRetry("https://api.example.com/data");
result.on?.("retry", ({ attempt, error }) => console.log(`Attempt ${attempt} failed: ${error}`));

Practice Questions

1. What’s the difference between readFile and createReadStream?

readFile loads the entire file into memory. createReadStream reads it in chunks — suitable for large files.

2. What is the EventEmitter pattern?

A publish-subscribe pattern where objects emit named events and listeners react. Used extensively in Node.js core modules.

3. What stream types exist?

Readable (source), Writable (destination), Duplex (both), Transform (modifies data). Use pipeline() to connect them.

4. What is Buffer used for?

Fixed-size chunk of raw memory for binary data — file contents, network packets, crypto operations.

5. Challenge: Build a file copy tool that uses streams.

const fs = require("node:fs");
const { pipeline } = require("node:stream/promises");

async function copyFile(src, dest) {
  await pipeline(fs.createReadStream(src), fs.createWriteStream(dest));
  console.log(`Copied ${src}${dest}`);
}
copyFile(process.argv[2], process.argv[3]);

FAQ

What’s the purpose of the node: prefix?
It explicitly identifies core module imports (require("node:fs")) vs. third-party packages (require("express")). Avoids naming conflicts.
How do I get user input from the terminal?
Use the readline module — createInterface, question(), and close() for interactive CLI apps.
What’s the difference between path.join() and path.resolve()?
join() concatenates path segments. resolve() gives an absolute path by resolving relative to the working directory.

Try It Yourself

Build a File Sync CLI Tool:

#!/usr/bin/env node
const fs = require("node:fs/promises");
const path = require("node:path");
const crypto = require("node:crypto");

async function hashFile(filePath) {
  const content = await fs.readFile(filePath);
  return crypto.createHash("sha256").update(content).digest("hex");
}

async function syncDirs(source, dest) {
  await fs.mkdir(dest, { recursive: true });
  const files = await fs.readdir(source);
  for (const file of files) {
    const srcPath = path.join(source, file);
    const destPath = path.join(dest, file);
    const stats = await fs.stat(srcPath);
    if (stats.isDirectory()) {
      await syncDirs(srcPath, destPath);
    } else {
      let needsCopy = true;
      try {
        const [srcHash, destHash] = await Promise.all([hashFile(srcPath), hashFile(destPath)]);
        needsCopy = srcHash !== destHash;
      } catch {}
      if (needsCopy) { await fs.copyFile(srcPath, destPath); console.log(`Synced: ${file}`); }
    }
  }
}
const [,, src, dest] = process.argv;
if (!src || !dest) { console.error("Usage: node file-sync.js <source> <dest>"); process.exit(1); }
syncDirs(src, dest).then(() => console.log("Sync complete"));

What’s Next

LessonDescription
https://tutorials.dodatech.com/backend/nodejs/express/Express.js framework
https://tutorials.dodatech.com/backend/nodejs/nodejs-database/MySQL and MongoDB
https://tutorials.dodatech.com/backend/nodejs/nodejs-advanced/Clustering, security, WebSockets
REST APIBuilding RESTful APIs
DockerContainerizing Node.js apps

What’s Next

Congratulations on completing this Nodejs Core Modules tutorial! Here’s where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro