From the Browser & Userland Libraries
Many habits and packages from older Node code or from front-end work have built-in replacements in modern Node. This appendix is a migration guide: browser JavaScript β Node, and the most common npm packages β the features that now ship with Node. Fewer dependencies mean fewer updates, fewer vulnerabilities and faster installs.
You will learn
- How browser concepts map to Node (window, DOM, storage, modules).
- Built-in replacements for chalk, axios, dotenv, nodemon, Mocha/Jest, uuid, rimraf, glob, yargs and more.
- How the Node.js "userland migrations" codemods automate some of this.
- What WebAssembly and Node-API (native addons) are, and when you'd meet them.
Browser β Node cheat sheet
| In the browser | In Node |
|---|---|
window | globalThis (works in both) |
document, the DOM | none β use a template string or library (jsdom for tests) |
localStorage | a file (fs), node:sqlite or a database |
<script type="module"> | "type": "module" in package.json |
location.search | new URL(req.url, base).searchParams (server), process.argv (CLI) |
fetch, URL, AbortController, TextEncoder, structuredClone, WebSocket, crypto.subtle, Blob, web streams | the same globals |
console.log β DevTools | console.log β stdout; --inspect for DevTools |
| user input: forms, events | process.stdin, readline, HTTP request bodies |
π§ Analogy: moving from a hotel to your own flat. In the browser you're a hotel guest: lots is provided (the DOM, storage, a window to look out of), but you can't touch the building (no file system, no raw sockets). Node is your own flat: no room service, but you have the keys to everything β and responsibility for locking the doors (security, Chapter 15).
Packages you may not need anymore
| Package | Built-in replacement | Chapter |
|---|---|---|
| chalk, kleur, ansi-colors | util.styleText | 2 |
| dotenv | node --env-file=.env, process.loadEnvFile() | 2 |
| nodemon | node --watch | 2 |
| yargs, minimist, commander (simple cases) | util.parseArgs | 2 |
| axios, node-fetch, got, request | fetch | 12 |
| ws (client) | WebSocket | 12 |
| mocha, jest, sinon (basic use) | node:test, node:assert, mock | 14 |
| nyc, c8 | --experimental-test-coverage | 14 |
| uuid | crypto.randomUUID() | β |
| rimraf, mkdirp, ncp | fs.rm({ recursive }), fs.mkdir({ recursive }), fs.cp | 9 |
| glob, fast-glob | fs.glob | 9 |
| lodash.clonedeep | structuredClone | β |
| sqlite3, better-sqlite3 (simple use) | node:sqlite | 16 |
| ts-node (for erasable TS) | node file.ts | App. A |
Big frameworks (Express, Fastify, Prisma, Zodβ¦) still earn their place. The candidates for removal are small utilities that only filled gaps Node has since closed.
Before and after
chalk β util.styleText
β Before: a dependency
β After: built in
styleText respects NO_COLOR, FORCE_COLOR and whether the output is a terminal (here it's captured, so no colour codes).
axios β fetch
The three differences to remember: stringify bodies and set headers yourself, check res.ok, parse the body explicitly. A 10-line wrapper (Chapter 12) restores the convenience.
dotenv, uuid, rimraf, structuredClone
Mocha β node:test
Mocha-style code often needs only the imports changed: describe, it, before, after, beforeEach and afterEach exist in node:test. Replace chai's expect(x).to.equal(y) with assert.equal(x, y), sinon spies with mock.fn(), and the mocha command with node --test. Jest users map jest.fn() β mock.fn(), jest.useFakeTimers() β t.mock.timers.enable(), and expect(...).toEqual β assert.deepEqual.
Automated migrations
The Node.js project maintains userland migrations: codemods that rewrite your code automatically, for example from chalk to util.styleText, from axios to fetch, from Mocha to the test runner, or to add correct .ts extensions to TypeScript imports. They run with the codemod tool:
Always run codemods on a clean git working tree, review the diff, and run your tests afterwards.
WebAssembly in Node
WebAssembly (Wasm) is a compact binary format that languages like Rust, C and Go can compile to. Node runs it with the same WebAssembly API as browsers, so you can use a fast image codec or parser written in another language without native compilation. This tiny module (written in bytes, normally produced by a compiler) exports an add function:
Native addons and Node-API
Some packages include C or C++ code compiled into a native addon (a .node file): database drivers, image processing (sharp), cryptography. Node-API is the stable C interface they use; its ABI stability means an addon compiled once keeps working across Node versions. As an app developer you mostly meet addons when npm install needs build tools (node-gyp, Python, a C++ compiler) or downloads prebuilt binaries for your platform. Prefer pure-JavaScript or WebAssembly packages when performance allows: they install anywhere. And under the permission model, addons must be explicitly allowed (--allow-addons).
More examples
yargs-style options with parseArgs
moment β Intl
Recap
- Browser β Node:
globalThisinstead ofwindow, no DOM or storage, the same web APIs (fetch,URL, streams,WebSocket), files and processes instead. - Built-ins replace many small packages:
styleText,--env-file,--watch,parseArgs,fetch,node:test,randomUUID,fs.rm/mkdir/cp/glob,structuredClone,node:sqlite. - Official codemods automate common migrations; review the diff and run the tests.
- WebAssembly runs portable compiled code; native addons use Node-API and need build tools or prebuilt binaries.
Exercises
Exercise 1 π’ easy map it
Name the built-in replacement for: (a) nodemon app.js; (b) require("dotenv").config(); (c) uuidv4(); (d) rimraf dist; (e) chalk.yellow("warn").
Show solution
(a) node --watch app.js; (b) node --env-file=.env app.js or process.loadEnvFile(); (c) crypto.randomUUID(); (d) fs.rm("dist", { recursive: true, force: true }); (e) util.styleText("yellow", "warn").
Exercise 2 π’ easy browser code
Which lines of this browser snippet fail in Node, and why? const theme = localStorage.getItem("theme"); const url = new URL(location.href); document.title = "Hi"; const res = await fetch("/api");
Show solution
localStorage, location and document don't exist in Node (ReferenceError). fetch exists, but "/api" is a relative URL with no page to resolve it against, so it throws an "invalid URL" TypeError β Node needs absolute URLs. new URL(...) itself works if given an absolute string.
Exercise 3 π’ easy convert
Rewrite with built-ins: const copy = _.cloneDeep(settings); const id = uuid.v4(); console.log(chalk.green(`saved ${id}`));
Show solution
Exercise 4 π’ easy explain
Give two reasons to remove a small utility dependency when Node has a built-in equivalent, and one reason you might keep it.
Show solution
Remove: fewer packages to audit and update (smaller supply-chain risk), and faster, smaller installs. Keep: when the package does meaningfully more than the built-in (e.g. yargs' generated help and sub-commands, axios interceptors used everywhere), or when you must support older Node versions that lack the built-in.
Exercise 5 π‘ medium axios habits
Write http.js exporting api.get(url) and api.post(url, body) that behave like axios: JSON in and out, throw on non-2xx with err.response = { status, data }, and a default 5 s timeout. Test it against a local server.
Show solution
Exercise 6 π‘ medium Jest β node:test
Translate this Jest test to node:test: const cb = jest.fn(); jest.useFakeTimers(); setTimeout(cb, 1000); jest.advanceTimersByTime(1000); expect(cb).toHaveBeenCalledTimes(1);
Show solution
Exercise 7 π‘ medium glob
Replace glob.sync("src/**/*.{js,ts}", { ignore: "**/*.test.*" }) with fs.glob, returning a sorted array.
Show solution
Exercise 8 π‘ medium a storage shim
Browser code uses localStorage. Write a file-backed createStorage(file) for Node with the same getItem/setItem/removeItem API (values are strings), saving synchronously so it behaves like the browser API.
Show solution
Sync writes are fine for a CLI or a test helper; in a server you'd use async I/O or a database.
Exercise 9 π΄ hard audit
You inherit a package.json with: express, dotenv, nodemon, axios, chalk, uuid, mocha, chai, sinon, rimraf, zod, pg. Which would you remove, keep or replace, and in what order would you do it safely?
Show solution
Keep: express (framework), zod (validation), pg (database driver) β no built-in equivalents. Replace: dotenv β --env-file; nodemon β --watch; chalk β styleText; uuid β randomUUID; rimraf β fs.rm; axios β fetch (with a small wrapper); mocha/chai/sinon β node:test/assert/mock. Order: dev-only tooling first (nodemon, rimraf, dotenv in scripts: zero runtime risk), then trivial runtime swaps (uuid, chalk), then the test stack (migrate files gradually; both runners can coexist), and axios last (most behaviour differences: errors, JSON, timeouts). One change per commit, tests green after each.
Exercise 10 π΄ hard WebAssembly
Modify the WebAssembly example's bytes so the exported function multiplies instead of adds (the opcode for i32.mul is 0x6c), and export it under the name mul. Call it with 6 and 7.
Show solution
The name is three bytes long like "add", so no section sizes change. Real projects never write bytes by hand: wat2wasm (text format) or a compiler (Rust's wasm-pack, Emscripten) produces them.