Node.js Built-in Modules Reference

Published by Programming Line in Node.js. Last updated .

Node.js ships with dozens of modules compiled directly into the runtime — no npm install required. These core modules cover file access, networking, streaming data, cryptography, operating-system introspection, and more. Knowing what’s already in the box saves you from pulling in a dependency for something Node already does well, and it’s the foundation every framework (Express, Fastify, and friends) is built on top of. This lesson is a tour of the most important core modules, how to import them the modern way, and where each one fits.

Overview: how core modules work

Every core module is imported with the node: prefix — for example import fs from "node:fs" or const os = require("node:os"). The prefix is not required for most core modules (you can still write require("fs")), but it is the current recommended style: it makes it instantly clear to a reader that the import is built-in rather than a package resolved from node_modules, and it is mandatory for a handful of modules that only exist with the prefix, like node:test.

Under the hood, core modules are not ordinary JavaScript files sitting on disk. They are compiled into the node binary itself (or lazily loaded from a internal snapshot), and many of them are thin JavaScript wrappers around C++ bindings that talk to libuv, the library that gives Node its asynchronous I/O. When you call an asynchronous fs function, the actual disk read happens on libuv’s thread pool (four threads by default) so it does not block the single JavaScript thread; the result is handed back to your callback or resolved promise once the event loop reaches the appropriate phase. Modules like net, http, and dns instead rely on the operating system’s non-blocking socket APIs, which libuv polls directly without needing the thread pool. Understanding this distinction — thread-pool-backed vs. OS-poll-backed — explains why, say, heavy fs work can starve other thread-pool consumers (like crypto.pbkdf2) but rarely affects raw socket throughput.

Most core modules come in more than one flavor. node:fs has a synchronous API (readFileSync), a callback API (readFile), and a promise API under node:fs/promises. node:stream has a promise-based helper module, node:stream/promises. In modern code you should default to the promise-based variant and async/await, reaching for sync or callback versions only when the situation specifically calls for them (a one-time synchronous read at startup, or matching an older callback-based API).

Reference table of key core modules

Module Purpose Common exports
node:fs / node:fs/promises Read, write, watch, and manage files and directories readFile, writeFile, mkdir, stat, watch
node:path Build and parse file paths in a cross-platform way join, resolve, basename, extname, dirname
node:os Query the host operating system platform, cpus, freemem, homedir
node:url Parse and build URLs (WHATWG URL API) URL, URLSearchParams, fileURLToPath
node:http / node:https Build HTTP(S) servers and make HTTP(S) requests createServer, request, get
node:events Emit and listen for custom events; base class for many Node APIs EventEmitter
node:stream / node:stream/promises Process data piece by piece instead of all at once Readable, Writable, pipeline
node:util General-purpose helpers promisify, inspect, parseArgs
node:crypto Hashing, encryption, HMAC, secure random values createHash, scrypt, randomBytes
node:child_process Spawn and manage external OS processes spawn, exec, fork
node:buffer Work directly with binary data Buffer
node:worker_threads Run CPU-bound JavaScript in parallel threads Worker, parentPort
process (global) Access environment, args, and control the running process process.env, process.argv, process.exit

Syntax

The general import form for a core module in ESM and CommonJS:

// ESM (import)
import moduleName from "node:module-name";
import { namedExport } from "node:module-name";

// CommonJS (require)
const moduleName = require("node:module-name");
const { namedExport } = require("node:module-name");
  • node: prefix — marks the import as a built-in core module, not a package from node_modules.
  • default vs named exports — some modules export a default object (path, os), others export named classes or functions ({ EventEmitter } from node:events).
  • promise submodules — several modules expose a /promises entry point (node:fs/promises, node:stream/promises, node:dns/promises) with the same functionality as async, awaitable functions instead of callbacks.

Examples

Example 1: combining path, os, and fs/promises

import { readFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";

async function main() {
  const configPath = path.join(import.meta.dirname, "config.json");

  try {
    const raw = await readFile(configPath, "utf8");
    console.log("Loaded config:", JSON.parse(raw));
  } catch (err) {
    console.error("Could not read config, using defaults:", err.message);
  }

  console.log("Platform:", os.platform());
  console.log("CPU cores:", os.cpus().length);
  console.log("Free memory (MB):", Math.round(os.freemem() / 1024 / 1024));
}

main();

Output:

Could not read config, using defaults: ENOENT: no such file or directory, open '.../config.json'
Platform: linux
CPU cores: 8
Free memory (MB): 5120

This mirrors a real startup routine: try to load a config file with fs/promises, fall back to defaults on failure, and log a snapshot of the host with os. import.meta.dirname (Node 20.11+) gives the directory of the current module without the __dirname global, which does not exist in ESM. The exact CPU count and memory numbers will differ by machine.

Example 2: events and url together

import { EventEmitter } from "node:events";
import { URL } from "node:url";

class RequestLogger extends EventEmitter {}

const logger = new RequestLogger();

logger.on("request-logged", (info) => {
  console.log(`[${info.method}] ${info.pathname}${info.search}`);
});

logger.on("error", (err) => {
  console.error("Logger error:", err.message);
});

function logRequest(method, rawUrl) {
  const url = new URL(rawUrl, "http://localhost:3000");
  logger.emit("request-logged", {
    method,
    pathname: url.pathname,
    search: url.search,
  });
}

logRequest("GET", "/users?active=true");
logRequest("POST", "/orders");

Output:

[GET] /users?active=true
[POST] /orders

EventEmitter is the base class behind streams, servers, and process signals — here it drives a tiny logging system. The WHATWG URL class parses a raw path (or full URL) into its parts (pathname, search, hostname, and so on) without any manual string splitting.

Example 3: hashing with crypto

import { randomBytes, scrypt } from "node:crypto";
import { promisify } from "node:util";

const scryptAsync = promisify(scrypt);

async function hashPassword(password) {
  const salt = randomBytes(16).toString("hex");
  const derivedKey = await scryptAsync(password, salt, 64);
  return `${salt}:${derivedKey.toString("hex")}`;
}

async function verifyPassword(password, stored) {
  const [salt, key] = stored.split(":");
  const derivedKey = await scryptAsync(password, salt, 64);
  return key === derivedKey.toString("hex");
}

async function main() {
  const stored = await hashPassword("correct horse battery staple");
  console.log("Stored hash:", stored.slice(0, 20) + "...");

  console.log("Correct password valid:", await verifyPassword("correct horse battery staple", stored));
  console.log("Wrong password valid:", await verifyPassword("wrong password", stored));
}

main();

Output:

Stored hash: 3f9a1c2e4b7d8f01a3...
Correct password valid: true
Wrong password valid: false

node:crypto‘s scrypt is a callback-based key-derivation function; wrapping it with util.promisify makes it await-friendly. This is the right way to hash passwords in Node — never store plain text or use a fast general-purpose hash like MD5 or SHA-256 alone for passwords, since those are designed to be fast and are therefore easy to brute-force.

Under the hood: sync, callback, and promise APIs

When you call fs.readFile(path, cb), Node validates the arguments synchronously, then hands the actual read off to libuv’s thread pool and returns immediately — the rest of your script keeps running. Once the OS-level read finishes on that background thread, libuv queues the completion, and on a later turn of the event loop’s poll phase, your callback runs on the main thread with the result. The promise-based fs/promises version does exactly the same underlying work; it just wraps the callback in a Promise so you can await it. The synchronous version, fs.readFileSync, skips the thread pool entirely and blocks the single JavaScript thread until the OS call returns — acceptable during startup, but disastrous inside a request handler because it freezes every other in-flight request. Sockets (net, http) behave a little differently: rather than using the thread pool, libuv registers the file descriptor with the OS’s native event notification mechanism (epoll on Linux, kqueue on macOS, IOCP on Windows) and is notified directly when data is ready — no worker thread involved.

Common Mistakes

Mistake 1: using __dirname in an ES module. __dirname and __filename are CommonJS-only globals; they don’t exist when a file is loaded as an ES module.

// wrong.mjs — throws ReferenceError: __dirname is not defined
import fs from "node:fs";

const configPath = __dirname + "/config.json";
const data = fs.readFileSync(configPath, "utf8");
console.log(data);

Fix it with import.meta.dirname (Node 20.11+) or, for older versions, derive it from import.meta.url:

import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const configPath = path.join(__dirname, "config.json");
const data = fs.readFileSync(configPath, "utf8");
console.log(data);

Mistake 2: reaching for a synchronous or whole-file fs call inside a request handler. fs.readFileSync (or reading an entire large file into memory with readFile) blocks or bloats memory under load. Prefer streaming with node:stream/promises‘ pipeline() for large files, and reserve sync calls for one-time startup work.

Mistake 3: creating an EventEmitter without an error listener. If an error event is emitted and nothing is listening for it, Node throws the error and crashes the process. Always attach an error handler on emitters, streams, and servers, as shown in Example 2.

Best Practices

  • Always import core modules with the node: prefix — it disambiguates built-ins from npm packages at a glance.
  • Default to the promise-based flavor of a module (fs/promises, stream/promises) and async/await; reach for sync APIs only for one-off startup work, and callback APIs only to match a legacy interface.
  • Use node:path (join, resolve) instead of manually concatenating strings with /, so paths work correctly on Windows too.
  • Prefer pipeline() from node:stream/promises over manual .pipe() chains — it forwards errors and cleans up automatically.
  • Never use a general-purpose hash for passwords; use node:crypto‘s scrypt or a dedicated library like bcrypt.
  • Always attach an error listener to any EventEmitter-based object (streams, servers, custom emitters).
  • Read the Node.js version notes for a module before using a newer method — for example import.meta.dirname requires Node 20.11+, and the global fetch requires Node 18+.

Practice Exercises

  • Write a script using node:os and node:process that prints a one-line system report: platform, total memory, uptime, and the Node.js version (process.version).
  • Using node:crypto, write a function fileChecksum(path) that streams a file through createHash("sha256") and resolves with the hex digest, without loading the whole file into memory at once.
  • Build a small EventEmitter subclass called TaskQueue that emits a "task-done" event with a result each time an async task finishes, and emits "error" if a task rejects. Test it with two or three fake async tasks.

Summary

  • Node.js core modules are compiled into the runtime and imported with the node: prefix — no installation needed.
  • Most I/O modules offer sync, callback, and promise variants; prefer the promise variant with async/await in modern code.
  • Async fs calls use libuv’s thread pool; sockets (http, net) use OS-level event notification instead, which is why blocking file I/O and blocking network I/O have different performance consequences.
  • node:events‘ EventEmitter underlies streams, servers, and many other Node APIs — always give it an error listener.
  • __dirname/__filename don’t exist in ESM — use import.meta.dirname or derive them from import.meta.url.
  • Use node:crypto for secure hashing and random values; never invent your own or use fast general-purpose hashes for passwords.