Built-in Modules
fs, path, http, url, crypto — what ships with Node.
Before you reach for an npm package, check Node's standard library first. It ships with everything you need to read files, hash data, build HTTP servers, parse URLs, and time operations down to the microsecond. The built-ins are battle-tested, free of supply-chain risk, and a lot faster to import than pulling 90MB of transitive dependencies. Let's tour the ones you'll actually use.
The node: prefix
Modern Node lets you import its built-ins with the explicit node: prefix. Always prefer it.
// Modern (preferred):
import { readFile } from "node:fs/promises";
import { createHash } from "node:crypto";
import { join } from "node:path";
// Legacy (still works, but ambiguous):
import { readFile } from "fs/promises";Why the prefix? Without it, if someone publishes an npm package called fs, it could shadow the built-in and your import becomes ambiguous. The node:prefix is unambiguous and tells the reader at a glance that it's standard library, not a dependency.
unicorn/prefer-node-protocol and your linter will nudge you toward the prefix on every built-in.fs/promises: reading and writing files
Node has two file system APIs. The old fs module uses callbacks and synchronous methods. The newer fs/promises is async and works perfectly with await. Use the promises one.
import { readFile, writeFile, mkdir } from "node:fs/promises";
import { join } from "node:path";
// Read a UTF-8 text file
const text = await readFile("notes.md", "utf-8");
console.log(text);
// Read as a Buffer (binary)
const bytes = await readFile("logo.png");
console.log("size:", bytes.byteLength, "bytes");
// Write a file (creates or overwrites)
await writeFile("output.json", JSON.stringify({ ok: true }, null, 2));
// Create nested directories
await mkdir(join("data", "exports"), { recursive: true });readFile(path) with no second argument returns a Buffer, not a string. If you wanted text, pass "utf-8"as the second arg. Otherwise you'll be printing raw bytes.path: build paths that work on Windows too
Never concatenate paths with + or string literals. macOS and Linux use / as the separator; Windows uses \. The path module hides the difference.
import { join, resolve, dirname, basename, extname } from "node:path";
import { fileURLToPath } from "node:url";
join("src", "lib", "index.js"); // "src/lib/index.js" (or backslashes on Windows)
resolve("./data/notes.md"); // absolute path from cwd
dirname("/a/b/c.txt"); // "/a/b"
basename("/a/b/c.txt"); // "c.txt"
basename("/a/b/c.txt", ".txt"); // "c"
extname("/a/b/c.txt"); // ".txt"
// In ESM, __dirname doesn't exist. Reconstruct it:
const __dirname = dirname(fileURLToPath(import.meta.url));url: parse URLs without regex
Need to crack open a URL? Don't roll your own. The URL constructor (globally available) and the node:url helpers do it correctly.
const u = new URL("https://api.example.com:8080/users?limit=10&active=true#bio");
u.protocol; // "https:"
u.host; // "api.example.com:8080"
u.hostname; // "api.example.com"
u.port; // "8080"
u.pathname; // "/users"
u.searchParams.get("limit"); // "10"
u.hash; // "#bio"
// Mutate and serialize
u.searchParams.set("limit", "50");
u.toString();
// "https://api.example.com:8080/users?limit=50&active=true#bio"crypto: hashes, randomness, UUIDs
The cryptomodule gives you cryptographic primitives. Two things you'll do constantly: generate random IDs, and hash strings.
import { createHash, randomUUID, randomBytes } from "node:crypto";
// UUID v4 - the cheapest unique ID
randomUUID();
// "a3c84b00-8d4d-4b2f-a09f-3d2e5b87cc1c"
// Random bytes (hex string useful for tokens)
randomBytes(32).toString("hex");
// "1f9c..."
// SHA-256 hash of a string
const hash = createHash("sha256").update("hello world").digest("hex");
// "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"argon2, bcrypt, or Node's built-in crypto.scrypt. SHA-256 is fine for file fingerprints, ETags, and content addressing.performance: measure stuff in microseconds
For timing operations, Date.now() only has millisecond resolution and can jump if your system clock changes. performance.now() is monotonic and sub-millisecond.
import { performance } from "node:perf_hooks";
const start = performance.now();
await doExpensiveThing();
const elapsed = performance.now() - start;
console.log("Took " + elapsed.toFixed(2) + "ms");
// Even better - the User Timing API
performance.mark("a");
await step1();
performance.mark("b");
await step2();
performance.mark("c");
performance.measure("step1", "a", "b");
performance.measure("step2", "b", "c");
console.log(performance.getEntriesByType("measure"));performance is also globally available in Node 16+, so the explicit import is optional. The User Timing measurements above show up in observability tools too.
http: a server in 6 lines
You'll mostly use Express or Hono for real apps, but the built-in httpmodule is what they're built on. Worth seeing once so you understand what frameworks add on top.
import { createServer } from "node:http";
const server = createServer((req, res) => {
if (req.url === "/health") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true }));
return;
}
res.writeHead(404);
res.end("not found");
});
server.listen(3000, () => {
console.log("listening on http://localhost:3000");
});Now run node server.js in one terminal and curl http://localhost:3000/health in another. You have a real web server. No frameworks. No npm install.
http module is your fallback when you need full control.Putting it together: a tiny file-hashing API
import { createServer } from "node:http";
import { readFile } from "node:fs/promises";
import { createHash, randomUUID } from "node:crypto";
import { join, basename } from "node:path";
import { performance } from "node:perf_hooks";
createServer(async (req, res) => {
const url = new URL(req.url, "http://localhost");
if (url.pathname === "/hash") {
const file = url.searchParams.get("file");
if (!file) {
res.writeHead(400);
return res.end(JSON.stringify({ error: "missing ?file=" }));
}
const safe = basename(file); // strip ../ tricks
const start = performance.now();
try {
const bytes = await readFile(join("./data", safe));
const sha = createHash("sha256").update(bytes).digest("hex");
const ms = (performance.now() - start).toFixed(2);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ id: randomUUID(), file: safe, sha, ms }));
} catch (err) {
res.writeHead(404);
res.end(JSON.stringify({ error: "not found" }));
}
return;
}
res.writeHead(404);
res.end("not found");
}).listen(3000);Six built-in modules, zero dependencies, an HTTP API that hashes files. That's the standard library's power in one screen.
Quiz
Why use the node: prefix when importing built-ins?
Recap
- Always import built-ins with the
node:prefix - it's unambiguous and lint-friendly. fs/promisesfor async file I/O. Pass"utf-8"if you want a string back.path.joinandpath.resolvefor cross-OS-safe paths. Never concatenate with+.new URL(...)parses URLs correctly.searchParamsbeats hand-rolled regex.crypto.randomUUID()for IDs.crypto.createHash("sha256")for fingerprints. Use a slow KDF for passwords.performance.now()for sub-ms, monotonic timing.http.createServeris the bedrock under every Node web framework.