Getting Started
REPL & Running Scripts
The REPL (Read-Eval-Print Loop) is an interactive shell for experimenting with Node.js code. Use node --watch for development auto-restart. ES modules require type:module in package.json or .mjs extension.
# start the REPL (interactive shell)
node
# run a script
node app.js
# run with ES modules (type: module in package.json)
node --input-type=module app.js
# execute inline code
node -e "console.log(process.version)"
# watch mode (auto-restart on change)
node --watch app.jsVersion & Runtime Flags
node --version prints the runtime version. --trace-warnings shows stack traces for runtime warnings. NODE_OPTIONS is a single env var holding CLI flags, handy inside containers or npm scripts. --inspect opens the Chrome DevTools debugger.
# check version
node --version # v20.11.0
node -p "process.versions.v8"
# enable warning stack traces
node --trace-warnings app.js
# pass NODE_OPTIONS (max ~128KB string)
NODE_OPTIONS="--max-old-space-size=4096" node app.js
# inspect memory usage at runtime
node --inspect app.jspackage.json & Project Init
npm init creates package.json. The "type" field decides the default module system: "module" enables ES module syntax in .js files, "commonjs" (default) keeps require/module.exports. "main" is the entry point when the package is required.
# scaffold a project (answer prompts)
npm init
# non-interactive defaults
npm init -y
# "type" controls module system
# "commonjs" (default) -> require/module.exports
# "module" -> import/export
{
"name": "my-app",
"version": "1.0.0",
"type": "module",
"main": "index.js",
"scripts": { "start": "node index.js" }
}CommonJS vs ES Modules
Node supports two module systems. CommonJS (require/module.exports) is synchronous and the historical default. ES Modules (import/export) are the modern standard. The file extension (.mjs/.cjs) or package.json "type" field decides which parser is used for .js files.
# CommonJS (.cjs or type:commonjs) ----
const fs = require('fs'); // import
module.exports = { greet: () => 'hi' }; // export
# ES Modules (.mjs or type:module) -------
import fs from 'fs'; // import
export function greet() { return 'hi'; } // named export
export default { greet }; // default export
# .mjs is always ESM, .cjs is always CJS
node file.mjs # forced ESM
node file.cjs # forced CJSGlobal Objects
Node provides globals like process, console, Buffer, and timers without imports. __dirname and __filename are CommonJS-only — in ES Modules use import.meta.url instead. global is the root scope object (like window in browsers).
# available everywhere, no import needed
global; // global namespace object
process; // env, argv, stdin/stdout
console; // log, error, warn, table
setTimeout; // schedule a callback
setInterval; // schedule repeating callback
Buffer; // binary data (Uint8Array)
queueMicrotask;// schedule microtask
structuredClone; // deep clone
# CommonJS-only globals (not in ESM)
__dirname; // current directory
__filename; // current file pathExecutable Scripts (Shebang)
A shebang line (#!/usr/bin/env node) lets a file run as a standalone command on Unix. After chmod +x, run ./cli.js directly. The package.json "bin" field maps a command name to a script, used by CLI tools published to npm so global install exposes the command.
#!/usr/bin/env node
// cli.js - make a file directly runnable
const name = process.argv[2] || 'world';
console.log(`Hello, ${name}!`);
# make it executable (Unix) and run directly
chmod +x cli.js
./cli.js Alice # Hello, Alice!
# or wire it up in package.json
{
"bin": { "mycli": "./cli.js" }
}Modules System
CommonJS require & exports
require() synchronously loads a CommonJS module and returns its module.exports. require resolves relative paths (./, ../), built-ins (fs), and node_modules. Always assign to module.exports (not exports) when replacing the whole export, because exports is just an alias for module.exports initially.
// math.js
function add(a, b) { return a + b; }
const PI = 3.14;
// export via module.exports
module.exports = { add, PI };
// or: exports.add = add; exports.PI = PI;
// app.js
const math = require('./math'); // .js optional
console.log(math.add(1, 2)); // 3
console.log(math.PI); // 3.14
// destructuring
const { add } = require('./math');
console.log(add(5, 7)); // 12ES Modules import/export
ES Modules use static import/export — bindings are live and hoisted. The default export is imported with any name (no braces); named exports must use braces with matching names. import * as creates a namespace object. ESM is async by design and the modern standard.
// math.mjs (or type:module)
export function add(a, b) { return a + b; }
export const PI = 3.14;
export default function square(x) { return x * x; }
// app.mjs
import square, { add, PI } from './math.mjs';
console.log(add(1, 2)); // 3
console.log(square(5)); // 25
// import all as namespace
import * as math from './math.mjs';
console.log(math.PI); // 3.14Dynamic import()
import() is a dynamic, async import usable in both CJS and ESM. It returns a Promise resolving to the module namespace, enabling lazy-loading and conditional imports — great for reducing startup time or loading optional features. Use it for code-splitting in servers too.
// dynamic import returns a Promise
async function loadPlugin(name) {
const mod = await import(`./plugins/${name}.mjs`);
return mod.default;
}
// lazy-load heavy modules on demand
if (needPdf) {
const { exportPdf } = await import('./pdf.mjs');
exportPdf(data);
}
// works in both CommonJS and ESM
import('./optional.mjs')
.then(mod => mod.run())
.catch(err => console.error('load failed', err));Built-in Modules
Node ships dozens of built-in modules — no npm install needed. Most expose callback APIs; many also offer promise variants under the 'module/promises' path (fs/promises, dns/promises, stream/promises). require('module') (no ./) resolves to built-ins first, then node_modules.
// core modules - no install, just require/import
const fs = require('fs'); // file system
const path = require('path'); // path handling
const http = require('http'); // HTTP server/client
const os = require('os'); // OS info
const crypto = require('crypto'); // hashing/encryption
const { EventEmitter } = require('events');
const { Buffer } = require('buffer');
const stream = require('stream'); // streams
const net = require('net'); // TCP
const child_process = require('child_process');
// some are promise-based (Node 10+)
const fsp = require('fs/promises'); // async/await fsModule Caching & Circular Deps
require() caches modules by resolved path, so the same module code runs once and exports are reused. Circular dependencies work but the first-loaded module sees an incomplete (partial) exports object of the second — design around them. delete require.cache to hot-reload during development.
// modules are cached after first load
// a.js
require('./b'); // b loads, sets module.exports
const b = require('./b'); // returns cached b (same object)
console.log(b === require('./b')); // true
// circular dependency: a -> b -> a
// a.js: const b = require('./b'); b.f();
// b.js: const a = require('./a'); // a is PARTIAL here!
// module.exports = { f: () => a.x };
// inspect the cache
console.log(require.cache); // map of loaded modules
delete require.cache[require.resolve('./b')]; // force reload__dirname & import.meta.url
__dirname and __filename are CommonJS-only globals giving the current folder/file path. ES Modules have no such globals — derive them from import.meta.url via fileURLToPath. import.meta.url is the file:// URL of the current module and the ESM replacement for __filename.
// CommonJS - synchronous path globals
console.log(__dirname); // /app/src
console.log(__filename); // /app/src/index.js
// ES Modules - no __dirname, use import.meta
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const dataPath = join(__dirname, 'data.json');
// import.meta also carries the module URL
console.log(import.meta.url); // file:///app/src/index.mjsnpm & pnpm
npm init & package.json
npm init scaffolds a package.json. -y skips prompts with defaults. npm set stores config in ~/.npmrc for future inits. Scoped packages (@org/name) are useful for organizations and avoiding name collisions on the registry.
# create package.json interactively
npm init
# accept all defaults
npm init -y
# pnpm equivalent
pnpm init
# set author/license globally
npm set init-author-name "Alice"
npm set init-license MIT
# scope packages with @username
npm init --scope=@myorgInstalling Dependencies
npm install (no args) installs all listed deps. With a name it adds to package.json. --save-dev puts it in devDependencies (test/build tools, not shipped). ^ allows minor+patch updates, ~ allows patch only. -g installs CLI tools globally.
# install everything in package.json
npm install # alias: npm i
pnpm install # alias: pnpm i
# add a runtime dependency -> dependencies
npm install express
pnpm add express
# add a dev-only dependency -> devDependencies
npm install --save-dev jest # npm i -D jest
pnpm add -D jest
# install a specific version
npm install [email protected]
npm install lodash@^4.17.0 # caret: 4.x
npm install lodash@~4.17.0 # tilde: 4.17.x
# global install (CLI tools)
npm install -g pm2
pnpm add -g pm2npm scripts
Scripts in package.json run via npm run <name>. start and test are special (npm start / npm test). pre/post hooks (pretest, postbuild) run automatically. Use -- to forward args to the underlying command. pnpm lets you skip the word "run" for custom scripts.
// package.json
{
"scripts": {
"start": "node index.js",
"dev": "node --watch index.js",
"test": "jest",
"build": "tsc && vite build"
}
}
# run a script
npm run dev # npm run-script dev
pnpm dev # pnpm has no "run" requirement
# "start" and "test" have shortcuts
npm start
npm test
# pre/post hooks run automatically
// "pretest": "npm run lint",
// "postbuild": "npm run deploy"
# pass extra args with --
npm run build -- --mode productionnpx & pnpm dlx
npx runs a package binary without a global install — it downloads, executes, and discards. Perfect for scaffolders (create-vite) and running local node_modules/.bin tools. --yes skips the install confirmation. pnpm dlx is the pnpm counterpart.
# run a one-off command without installing globally
npx create-vite my-app
npx cowsay "hello"
# pin a version
npx prettier@3 --write .
# pnpm equivalent
pnpm dlx create-vite my-app
# execute a local bin from node_modules
npx jest # runs ./node_modules/.bin/jest
# never prompt, always download latest
npx --yes create-react-app my-appVersioning & lockfiles
SemVer: MAJOR for breaking changes, MINOR for features, PATCH for fixes. ^ (caret) is npm's default and allows minor+patch updates; ~ (tilde) only patch. Lockfiles (package-lock.json) freeze the full dependency tree for reproducible installs — use npm ci in CI for speed and safety.
# semantic versioning: MAJOR.MINOR.PATCH
# 1.4.2 -> 1:breaking 4:feature 2:fix
"^1.4.2" # >=1.4.2 <2.0.0 (caret, default)
"~1.4.2" # >=1.4.2 <1.5.0 (tilde)
"1.4.2" # exact 1.4.2
">=1.4.0 <2.0.0" # range
# lockfiles pin the installed tree
npm install # writes/updates package-lock.json
pnpm install # writes pnpm-lock.yaml
yarn install # writes yarn.lock
# install exactly as locked (CI)
npm ci # clean install from lockfile
# find outdated deps
npm outdated
npm update # update within ^ rangesPublishing & Lifecycle
npm publish uploads your package to the registry. npm version bumps the version, creates a git tag, and commits. Lifecycle hooks (prepublishOnly, postpublish) automate build/test on release. Unpublish is only allowed within 72 hours — prefer npm deprecate for old versions.
# login once
npm login
# publish to the registry
npm publish
npm publish --access public # scoped default restricted
# bump version (updates package.json + tag + commit)
npm version patch # 1.0.0 -> 1.0.1
npm version minor # 1.0.0 -> 1.1.0
npm version major # 1.0.0 -> 2.0.0
# lifecycle scripts (in package.json)
// "prepublishOnly": "npm test && npm run build",
// "prepublish": "npm run build",
// "postpublish": "git push --tags"
# unpublish within 72h
npm unpublish [email protected]File System (fs)
Reading Files
fs offers sync (readFileSync), callback (readFile), and promise (fs/promises readFile) APIs. Sync calls block the event loop — avoid in servers. fs/promises is the modern choice for async/await. Without an encoding, readFile returns a Buffer.
const fs = require('fs');
// synchronous (blocks the event loop)
const text = fs.readFileSync('data.txt', 'utf8');
const buf = fs.readFileSync('image.png'); // Buffer
// asynchronous with callback (error-first)
fs.readFile('data.txt', 'utf8', (err, data) => {
if (err) return console.error(err);
console.log(data);
});
// async with fs/promises (preferred)
const fsp = require('fs/promises');
async function read() {
const text = await fsp.readFile('data.txt', 'utf8');
return text;
}Writing Files
writeFile replaces the whole file by default. The flag option changes behavior: 'a' appends, 'wx' fails (EEXIST) if the file already exists — useful for atomic file creation. JSON should be stringified before writing. Sync writes block; prefer fs/promises.
const fs = require('fs');
const fsp = require('fs/promises');
// sync
fs.writeFileSync('out.txt', 'hello');
// async callback
fs.writeFile('log.txt', 'line1\n', err => {
if (err) throw err;
});
// promise
await fsp.writeFile('config.json', JSON.stringify(cfg, null, 2));
// flags: 'w' write (default), 'a' append, 'wx' fail if exists
await fsp.writeFile('new.txt', 'x', { flag: 'wx' }); // throws if existsDirectories
mkdir with { recursive: true } mimics mkdir -p. readdir lists entries; { withFileTypes: true } returns Dirent objects whose isDirectory()/isFile() avoid extra stat calls. fsp.rm with recursive+force replaces rmdir for non-empty dirs (Node 14+).
const fsp = require('fs/promises');
// create (recursive like mkdir -p)
await fsp.mkdir('a/b/c', { recursive: true });
// list entries
const entries = await fsp.readdir('.');
// ['app.js', 'data', 'package.json']
// list with type info
const detailed = await fsp.readdir('.', { withFileTypes: true });
detailed.forEach(e => console.log(e.name, e.isDirectory()));
// remove
await fsp.rmdir('empty'); // empty dir only
await fsp.rm('folder', { recursive: true, force: true }); // rm -rf
// sync versions exist too: mkdirSync, readdirSync, rmSyncFile Stats & Metadata
stat() returns a Stats object with size, mtime, and type helpers. Don't use fs.exists (deprecated); use access() in a try/catch for race-free existence checks — though opening the file directly is safer. fsWatch emits change events but can be inconsistent across platforms.
const fsp = require('fs/promises');
const stats = await fsp.stat('data.txt');
stats.isFile(); // true
stats.isDirectory(); // false
stats.size; // bytes
stats.mtime; // Date modified
stats.birthtime; // Date created
stats.mode; // permission bits
// check existence (avoid existsSync for races)
try {
await fsp.access('config.json');
console.log('exists');
} catch {
console.log('missing');
}
// watch for changes
const watcher = fs.watch('.');
watcher.on('change', (eventType, filename) => {});fs.promises API
fs/promises (Node 10+) provides promise-based versions of all fs methods, ideal for async/await. opendir() returns an async iterator for memory-efficient directory traversal. Combine with path.join for portable paths. All methods throw on error — wrap in try/catch.