Node.js at a Glance
Node.js runs JavaScript outside the browser using the V8 engine. It is commonly used for servers, APIs, command-line tools, build tooling, automation, real-time applications, and backend services.
macOS default recommendation: install an LTS Node version through a version manager such as nvm. For production work, stay on an Active or Maintenance LTS line unless a project explicitly requires Current.
macOS Installation & Version Management
Use one primary installation strategy. Mixing a system installer, Homebrew Node, and a version manager can make which node point to the wrong binary.
The three columns below read as Use / Install / Notes.
Use: best for developers who need to switch Node versions per project. npm's own documentation strongly recommends a Node version manager.
Install:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.6/install.sh | bash
source ~/.zshrc
command -v nvm
nvm install 24
nvm use 24
node -v
npm -vNotes: macOS uses zsh by default. If ~/.zshrc does not exist, create it with touch ~/.zshrc, then rerun the installer or add the nvm load lines manually.
Use: simple single-version setup when you cannot or do not want to use a version manager.
Download: nodejs.org/en/download
Select the LTS release for normal development. Run the macOS installer, then verify:
node -v
npm -v
which nodeNotes: npm documentation prefers a version manager because installer-based global packages can lead to permissions problems. Do not use sudo npm install -g as a routine fix.
Use: convenient when you deliberately manage Node through Homebrew rather than nvm.
LTS 24:
brew install node@24
brew info node@24The unversioned formula currently tracks Node 26:
brew install nodeNotes: node@24 is keg-only. Follow Homebrew's displayed PATH instructions if the executable is not found. For projects needing several Node lines, nvm is usually simpler.
Version & nvm Commands
Use these when setting up a Mac, switching projects, or checking which Node binary is active.
Confirm the versions currently on PATH.
node -v
npm -v
which node
which npmnvm is a shell function, so use command -v.
command -v nvmInstall and activate the Node 24 line.
nvm install 24
nvm use 24
node -vShows versions already installed through nvm.
nvm lsSee supported LTS lines available to install.
nvm ls-remote --ltsChange the Node version for the current shell.
nvm use 24New Terminal sessions can default to the selected major.
nvm alias default 24Commit .nvmrc so collaborators know the intended Node line.
echo "24" > .nvmrc
nvm useUseful when Homebrew or an installer copy is unexpectedly taking precedence.
nvm which current
which node
node -p "process.execPath"Start a Node Project
Left side shows the action; right side shows the implementation or resulting project structure.
Create a clean project directory and a package.json.
mkdir my-node-app
cd my-node-app
git init
npm init -yAdd a project Node version file.
echo "24" > .nvmrc
nvm useFor a new ESM project, add this to package.json.
{
"type": "module"
}Put repeatable tasks behind project scripts.
{
"scripts": {
"start": "node src/app.js",
"dev": "node --watch src/app.js",
"test": "node --test"
}
}A small service does not need a complex architecture.
my-node-app/
├── src/
│ ├── app.js
│ ├── routes/
│ └── services/
├── test/
├── .env
├── .gitignore
├── .nvmrc
├── package.json
└── package-lock.jsonDo not commit dependencies, secrets, logs, or OS metadata.
node_modules/
.env
*.log
.DS_Store
coverage/package.json Quick Reference
package.json is the project manifest. Node uses some fields directly; npm uses many additional fields.
name identifies the package; version follows semantic versioning. For an application that should never be published accidentally, use "private": true.
{ "name": "my-app", "version": "1.0.0", "private": true }"type": "module" makes nearby .js files ESM. "type": "commonjs" makes them CommonJS. Be explicit.
{ "type": "module" }Named development commands run with npm run <name>. start and test also have npm shorthand commands.
{
"scripts": {
"start": "node src/app.js",
"dev": "node --watch src/app.js",
"test": "node --test"
}
}Packages required when the application runs.
{ "dependencies": { "express": "^5.0.0" } }Development-only tools such as linters, test helpers, build tools, or TypeScript tooling.
{ "devDependencies": { "typescript": "^5.9.0" } }Declare the Node versions the package expects. Enforcement depends on package-manager settings and environment.
{ "engines": { "node": ">=24" } }main is the older single entry point. For packages, Node recommends modern exports for a deliberate public interface and subpath control.
{
"exports": {
".": "./src/index.js",
"./utils": "./src/utils.js"
}
}Essential Node Commands
These are runtime commands from the Node executable itself.
Execute the entry file once.
node src/app.jsStart an interactive JavaScript prompt. Exit with .exit or Control-D.
nodeUseful for quick checks from Terminal.
node -e "console.log(process.platform)"-p evaluates and prints the result.
node -p "process.version"Parse a JavaScript file without executing it.
node --check src/app.jsRestart when the entry point or imported modules change. Watch mode is stable.
node --watch src/app.jsOn macOS, explicitly watch directories. This option is supported on macOS and Windows.
node --watch-path=./src --watch-path=./config src/app.jsLoad environment variables from a file before the application starts.
node --env-file=.env src/app.jsDo not fail when the env file is absent.
node --env-file-if-exists=.env.local src/app.jsUse Node's built-in test runner.
node --testStart with the debugger enabled. Chrome DevTools or an IDE can attach.
node --inspect src/app.jsPause before application code executes.
node --inspect-brk src/app.jsnode --run is a fast, intentionally limited package-script runner. It is not a full replacement for npm lifecycle behavior.
node --run test
node --run test -- --verboseList runtime flags available in the installed Node version.
node --helpnpm / npx Essentials
npm ships with standard Node installations and handles dependencies and project scripts. This section contains the Node-adjacent commands you reach for most often.
Interactive or accept defaults.
npm init
npm init -yInstall everything declared in package.json.
npm installSaved to dependencies by default.
npm install expressSaved to devDependencies.
npm install -D eslintRemoves the package and updates package metadata/lock data.
npm uninstall expressRuns a command from the scripts object.
npm run dev
npm test
npm startEverything after -- is passed to the script.
npm run test -- --watchnpx is the familiar interface for npm package execution.
npx eslint .
npm exec -- eslint .Use in CI or when you want the lockfile to drive a clean install.
npm ciCompare installed, wanted, and latest versions.
npm outdatedUpdates packages according to declared semver ranges.
npm updateCheck the dependency tree against known vulnerability data. Review fixes before using force.
npm audit
npm audit --audit-level=highSee direct dependencies or why a package exists.
npm ls --depth=0
npm explain <package>Inspect registry data without installing the package.
npm view <package>
npm view <package> versions --jsonDiscover commands and command-specific documentation.
npm help
npm help installModern Modules: ESM & CommonJS
Node supports both module systems. For new code, explicit ESM is usually the clearest choice. Keep CommonJS where compatibility requires it.
Enable with "type": "module", use .mjs, or another explicit ESM marker.
// math.js
export function add(a, b) {
return a + b;
}
// app.js
import { add } from './math.js';
console.log(add(2, 3));Use .cjs or "type": "commonjs" when you need CommonJS explicitly.
// math.cjs
exports.add = (a, b) => a + b;
// app.cjs
const { add } = require('./math.cjs');Use the node: prefix to make it obvious an import is a Node built-in rather than an npm package.
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';On Node 24, import.meta.dirname and import.meta.filename are stable for local file modules.
console.log(import.meta.dirname);
console.log(import.meta.filename);
const file = new URL('./data.json', import.meta.url);Load a module only when needed. import() works in both ESM and CommonJS.
const module = await import('./feature.js');
module.run();Modern ESM imports JSON with an import attribute.
import config from './config.json' with { type: 'json' };Core APIs You Reach for Often
Node includes a large standard library. Prefer a built-in API when it cleanly solves the problem; add a package when the ecosystem library provides meaningful value.
Promise-based file access fits naturally with async/await.
import { readFile, writeFile } from 'node:fs/promises';
const text = await readFile('notes.txt', 'utf8');
await writeFile('copy.txt', text);Create cross-platform paths rather than manually concatenating slashes.
import path from 'node:path';
const file = path.join(import.meta.dirname, 'data', 'items.json');Access environment variables, arguments, platform information, working directory, and exit status.
import process from 'node:process';
console.log(process.env.NODE_ENV);
console.log(process.argv.slice(2));
console.log(process.cwd());
process.exitCode = 1;Use the WHATWG URL API for URLs and query parameters.
const url = new URL('https://example.com/search?q=node');
console.log(url.hostname);
console.log(url.searchParams.get('q'));Use Node's crypto APIs for secure randomness and identifiers.
import { randomUUID, randomBytes } from 'node:crypto';
console.log(randomUUID());
console.log(randomBytes(16).toString('hex'));Node's binary-data type is common with files, sockets, streams, and encodings.
const bytes = Buffer.from('hello', 'utf8');
console.log(bytes);
console.log(bytes.toString('base64'));Many Node APIs build on event-driven patterns.
import { EventEmitter } from 'node:events';
const bus = new EventEmitter();
bus.on('ready', () => console.log('ready'));
bus.emit('ready');Streams avoid loading an entire large input into memory at once.
import { createReadStream, createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
await pipeline(
createReadStream('large.dat'),
createWriteStream('copy.dat')
);Modern Node includes a browser-compatible global fetch(); no request package is needed for ordinary HTTP requests.
const response = await fetch('https://api.example.com/items');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();HTTP & API Implementation
Node can serve HTTP without a framework. A framework such as Express or Fastify becomes useful when routing, middleware, validation, plugins, or application structure justify the dependency.
Built-in server with a plain-text response.
import { createServer } from 'node:http';
const server = createServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' });
res.end('Hello from Node');
});
server.listen(3000, () => {
console.log('http://localhost:3000');
});Set the content type explicitly and serialize the object.
const body = JSON.stringify({ ok: true });
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });
res.end(body);For a tiny service, basic routing can be explicit.
const url = new URL(req.url, `http://${req.headers.host}`);
if (req.method === 'GET' && url.pathname === '/api/items') {
// handle request
}Use the built-in global fetch for outbound API calls.
const res = await fetch('https://api.example.com/items');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const items = await res.json();Serialize the body and declare JSON content.
const res = await fetch('https://api.example.com/items', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ name: 'Bird' })
});Use AbortController for cancellation and timeout behavior.
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
const res = await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timer);
}Stop accepting new connections before the process exits.
const shutdown = () => {
server.close((err) => {
if (err) {
console.error(err);
process.exitCode = 1;
}
});
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);Environment, Arguments & Process
Keep machine-specific configuration outside source code. Do not commit secrets in .env.
Simple key/value configuration. Add the file to .gitignore if it contains secrets.
PORT=3000
NODE_ENV=development
API_URL=https://api.example.comNode can load env files directly; a dotenv package is not required for this basic use case.
node --env-file=.env src/app.jsAll environment values arrive as strings or undefined; validate and convert them.
const port = Number(process.env.PORT ?? 3000);
const mode = process.env.NODE_ENV ?? 'development';The first two values are the Node executable and entry file. Application arguments start at index 2.
const args = process.argv.slice(2);
console.log(args);
// node app.js --verboseprocess.cwd() is where the command was launched, not necessarily the module's directory.
console.log(process.cwd());
console.log(import.meta.dirname);Prefer setting process.exitCode when normal cleanup can finish.
try {
await main();
} catch (error) {
console.error(error);
process.exitCode = 1;
}Testing, Watch & Debugging
Node includes a test runner and inspector, so a small project can begin testing and debugging without adding a framework.
Create a file such as test/math.test.js.
import test from 'node:test';
import assert from 'node:assert/strict';
function add(a, b) {
return a + b;
}
test('adds two numbers', () => {
assert.equal(add(2, 3), 5);
});Run Node's built-in test runner.
node --testRerun tests as related files change.
node --test --watchAttach Chrome DevTools, VS Code, or another inspector-compatible debugger.
node --inspect src/app.jsUseful when startup code is the problem.
node --inspect-brk src/app.jsSimple built-in timing is useful before reaching for a profiler.
console.time('load');
await loadData();
console.timeEnd('load');Ask Node to show where warnings originate.
node --trace-warnings src/app.jsNode 24 + TypeScript
Node 24 can execute TypeScript containing erasable type syntax through built-in type stripping. This is useful for lightweight scripts, but it is not a replacement for TypeScript type checking.
On current Node 24 LTS, type stripping is stable. Inline types are removed at runtime.
// script.ts
const name: string = 'Node';
console.log(name);
// run
node script.tsNode's built-in type stripping does not type-check and does not read tsconfig.json.
# still type-check separately
npx tsc --noEmitFor features needing transformation or full tsconfig-aware tooling, use a tool such as tsx.
npm install -D tsx
npx tsx src/app.ts
# or
node --import=tsx src/app.tsWhen using built-in stripping, use the type keyword for imports that exist only to the type system.
import type { User } from './types.ts';
import { save, type SaveOptions } from './save.ts';Syntax requiring JavaScript generation—such as some enums, runtime namespaces, and parameter properties—requires transformation rather than simple stripping.
// use full TypeScript tooling when transformed syntax is requiredBest Implementation Patterns
These patterns keep Node projects easier to upgrade, debug, deploy, and hand to another developer.
Pin the intended major/version and upgrade deliberately. Current releases are useful for evaluating upcoming features, not as an automatic production default.
Set "type": "module" or "type": "commonjs". Avoid ambiguous projects that rely on source detection.
Commit package-lock.json for applications so installs are reproducible. Use npm ci in CI when appropriate.
Install project tools in devDependencies and call them through npm scripts or npx. Avoid depending on a globally installed tool version.
Synchronous file and CPU-heavy work blocks the event loop. Use promise/callback APIs for I/O and worker threads or another process for substantial CPU-bound work.
Modern Node already includes fetch, test tooling, env-file loading, crypto, streams, URL handling, file APIs, and watch mode. Fewer dependencies can mean a smaller maintenance and security surface.
Check required environment values once when the application starts. Convert numbers/booleans explicitly and fail with a useful message.
Catch errors where you can log context, return an HTTP response, retry deliberately, or terminate cleanly. Do not silently discard rejected promises.
Keep transport/routing, business logic, persistence, and external-service access separable. This makes tests and replacement easier without forcing a large framework architecture.
Handle SIGINT/SIGTERM, stop accepting new work, close servers/database pools, and allow in-flight work to finish when practical.
Review dependencies, use supported Node versions, run npm audit, keep secrets out of source control, and review package install scripts when risk warrants it.
Troubleshooting Fast Map
Check the active runtime and project state before reinstalling everything.
Common Workflow Recipes
Short repeatable sequences for the situations you encounter most often.
Create, pin, initialize, and start.
mkdir app && cd app
nvm use 24
npm init -y
mkdir src
touch src/app.js
node src/app.jsUse the project's Node version and lockfile.
git clone <repo>
cd <repo>
nvm use
npm ci
npm test
npm run devRun a repeatable dev script rather than remembering long commands.
npm run dev
# usually maps to:
node --watch src/app.jsRun the project's checks locally.
npm test
npm run lint --if-present
npm auditChange runtime deliberately, reinstall, then test.
nvm install 24
nvm use 24
node -v
npm ci
npm testFast diagnosis for platform and binary mismatches.
node -p "process.version"
node -p "process.platform"
node -p "process.arch"
node -p "process.execPath"
npm -vOfficial Documentation & Downloads
Prefer the official Node/npm documentation for runtime behavior and command syntax. Use the nvm repository for nvm-specific commands.