Intro
A local Node.js lab is a safe workspace on your machine for testing, troubleshooting, and iterative learning without touching production or team environments. By fixing versions, isolating changes, and adding observable checks, you make results repeatable and issues easy to diagnose.
In this guide you will:
- Establish a clean, versioned Node.js environment with a version manager.
- Create a dedicated lab directory containing three small, self-contained projects.
- Run and validate each project with concrete commands.
- Diagnose common issues and recover safely to a known-good state.
- Adopt a short daily runbook so sessions are fast and predictable.
Why this matters:
- A clear local setup reduces rework by keeping learning separate from deploy decisions and helping you iterate without side effects.
- Your first pilot should be narrow and measurable so you can observe behavior locally before any wider adoption.
Version and Environment Inventory
Before you install anything, capture what you have. This baseline helps you reproduce results, and it is the first thing you will review when something behaves unexpectedly.
Record your OS and architecture:
- macOS:
sw_vers && uname -m - Linux:
lsb_release -a 2>/dev/null || cat /etc/os-release; uname -m - Windows (PowerShell):
Get-ComputerInfo | Select-Object OsName, OsVersion, OsArchitecture
Record Node.js and npm if present:
node -v || echo 'node not installed'npm -v || echo 'npm not installed'
Record your shell and the path to Node (if present):
- Unix shells:
echo $SHELL && which node || command -v node - Windows PowerShell:
$PSVersionTable.PSVersion; Get-Command node -ErrorAction SilentlyContinue
Choose your lab root and default ports:
- Suggested lab root (Unix):
~/lab/node - Suggested lab root (Windows):
C:\lab\node - Suggested ports: 3000 and 3001 for HTTP APIs
Constructed example inventory values (for illustration):
- OS and arch: macOS 13.6 on arm64
- Node.js: v18.19.1
- npm: 9.6.7
- Shell: /bin/zsh
- Node path:
/Users/alex/.nvm/versions/node/v18.19.1/bin/node - Lab root:
/Users/alex/lab/node - Ports: 3000, 3001
Safe Configuration Path
Make implementation choices that keep the lab predictable and low-risk.
Principles:
- Use Node.js LTS. Favor stability over bleeding edge.
- Manage versions per-user with nvm (Unix) or nvm-windows (Windows). Do not rely on system Node.
- Use npm (bundled with Node) to avoid extra moving parts.
- Work in a dedicated lab directory to prevent cross-project contamination.
- Avoid sudo or Administrator when installing project dependencies.
Steps:
- Install a Node.js version manager
- macOS/Linux (review the script locally before running):
# install nvm (review script before executing)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
# reload shell (pick one)
. "$HOME/.nvm/nvm.sh" || source "$HOME/.bashrc" || source "$HOME/.zshrc"
# verify
command -v nvm && nvm --version
- Windows: Install nvm-windows via its installer. After install, open a new PowerShell and run:
nvm version
- Install Node.js LTS and set it as default
- macOS/Linux:
nvm install --lts
nvm use --default 'lts/*'
node -v
npm -v
- Windows PowerShell:
nvm list available # inspect available LTS
nvm install lts # or specify a version, e.g., nvm install 18.19.1
nvm use lts
node -v
npm -v
- Create a clean lab root
- macOS/Linux:
mkdir -p ~/lab/node && cd ~/lab/node
pwd
- Windows PowerShell:
New-Item -Type Directory -Path C:\lab\node -Force | Out-Null
Set-Location C:\lab\node
Get-Location
- Decide baseline ports and check availability
- Unix:
lsof -i :3000 || echo 'port 3000 free'
- Windows:
netstat -ano | findstr :3000
Practical Examples
You will create three small projects. Each lives in its own folder under the lab root and runs independently.
1) 01-hello-http (no external dependencies)
Create the project:
mkdir 01-hello-http && cd 01-hello-http
npm init -y
npm pkg set scripts.start='node index.js'
Create index.js:
// index.js
const http = require('http');
const PORT = process.env.PORT || 3000;
const server = http.createServer((req, res) => {
const ts = new Date().toISOString();
console.log(`${ts} ${req.method} ${req.url}`);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true, message: 'hello-http', time: Date.now() }));
});
server.listen(PORT, () => {
console.log(`hello-http listening on http://localhost:${PORT}`);
});
Run it:
npm start
Test it in another terminal:
curl -s http://localhost:3000 | jq .
Expected response (example):
{
"ok": true,
"message": "hello-http",
"time": 1699999999999
}
2) 02-express-api (minimal routing)
Create the project:
cd ..
mkdir 02-express-api && cd 02-express-api
npm init -y
npm install express
npm pkg set scripts.start='node server.js'
Create server.js:
// server.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3001;
app.use(express.json());
app.get('/health', (req, res) => res.json({ status: 'ok', at: Date.now() }));
app.get('/add', (req, res) => {
const a = Number(req.query.a || 0);
const b = Number(req.query.b || 0);
res.json({ a, b, sum: a + b });
});
app.listen(PORT, () => {
console.log(`express-api listening on http://localhost:${PORT}`);
});
Run and test:
npm start
# new terminal
curl -s http://localhost:3001/health
curl -s "http://localhost:3001/add?a=2&b=5"
Expected responses (examples):
{"status":"ok","at":1699999999999}
{"a":2,"b":5,"sum":7}
3) 03-file-io-and-env (fs + dotenv)
Create the project:
cd ..
mkdir 03-file-io-and-env && cd 03-file-io-and-env
npm init -y
npm install dotenv
npm pkg set scripts.start='node app.js'
Add .env and a sample data file:
# .env (example)
APP_NAME=lab-fs
DATA_FILE=data.json
# data.json (example)
{
"items": [1, 2, 3, 4]
}
Create app.js:
// app.js
require('dotenv').config();
const fs = require('fs');
const path = require('path');
const appName = process.env.APP_NAME || 'lab-fs';
oneof
const dataFile = process.env.DATA_FILE || 'data.json';
const p = path.resolve(__dirname, dataFile);
if (!fs.existsSync(p)) {
console.error(`missing data file: ${p}`);
process.exit(1);
}
const raw = fs.readFileSync(p, 'utf8');
const parsed = JSON.parse(raw);
const items = Array.isArray(parsed.items) ? parsed.items : [];
const sum = items.reduce((a, b) => a + b, 0);
console.log(JSON.stringify({ appName, file: p, count: items.length, sum }));
Run and test:
npm start
Expected output (example):
{"appName":"lab-fs","file":"/path/03-file-io-and-env/data.json","count":4,"sum":10}
Tip: Add .env to .gitignore in this project so you do not commit secrets.
Verification and Diagnostics
Verification proves your lab behaves as intended. Diagnostics help you find why it does not.
Verify versions are the ones you expect:
node -v
npm -v
Verify listeners are active:
- For 01-hello-http on port 3000 (Unix):
lsof -i :3000 | grep LISTEN || echo 'no listener on 3000'
- For 02-express-api on port 3001 (Unix):
lsof -i :3001 | grep LISTEN || echo 'no listener on 3001'
- Windows alternative:
netstat -ano | findstr LISTENING | findstr :3000
netstat -ano | findstr LISTENING | findstr :3001
Verify endpoints return expected payloads:
curl -s http://localhost:3000 | jq .
curl -s http://localhost:3001/health | jq .
curl -s "http://localhost:3001/add?a=10&b=15" | jq .
Turn on basic debugging when needed:
- Use Node inspector:
node --inspect index.js(or entry file), then openchrome://inspectand attach. - Add targeted logs around suspected trouble spots rather than verbose global logging.
Quick smoke tests with Node's built-in test runner (optional):
# inside 01-hello-http
mkdir -p test
cat > test/smoke.test.js <<'EOF'
const test = require('node:test');
const assert = require('node:assert');
test('math still works', () => {
assert.strictEqual(2 + 3, 5);
});
EOF
node --test
Failure Modes and Recovery
Common issues and practical fixes:
- Symptom: EADDRINUSE on port 3000
- Likely cause: Another process is using the port
- Fix (Unix):
lsof -i :3000 -sTCP:LISTEN -t | xargs kill -9or change thePORTenv var - Fix (Windows):
netstat -ano | findstr :3000thentaskkill /PID <pid> /F
- Symptom: MODULE_NOT_FOUND
- Likely cause: Dependency not installed or wrong import path
- Fix: Run
npm installin the project folder; checkrequire('./file')vsrequire('module')
- Symptom: SyntaxError about import/export
- Likely cause: ESM vs CommonJS mismatch
- Fix: Use CommonJS
requirein these examples, or opt into ESM by adding"type": "module"and usingimportconsistently
- Symptom: Permission denied when installing
- Likely cause: Installing globally or into a protected path
- Fix: Install per-project without sudo/Administrator. Ensure the lab root is in your user home
- Symptom: Unexpected Node behavior
- Likely cause: Wrong Node version active
- Fix:
nvm use 'lts/*'and verify withnode -v. Optionally pin engines inpackage.json
- Symptom: App reads wrong config
- Likely cause: Environment variables not loaded
- Fix: Check
.envlocation; ensurerequire('dotenv').config()runs before readingprocess.env
- Symptom: Stale or broken dependencies
- Likely cause: Corrupted
node_modulesor cache - Fix:
rm -rf node_modules package-lock.json && npm cache verify && npm ci
Rollback and recovery procedures:
- Switch Node versions safely:
nvm ls
nvm use 'lts/*'
nvm alias default 'lts/*'
- Restore a project to a clean state:
# from the project folder
rm -rf node_modules package-lock.json
npm ci # clean install from lockfile
- Revert local changes without a VCS:
# keep a copy of a working project folder as a backup
cp -a 01-hello-http 01-hello-http.bak
# if the project breaks, remove and restore from backup
rm -rf 01-hello-http && cp -a 01-hello-http.bak 01-hello-http
- Free a stuck port when the process is unknown:
# Unix
lsof -i :3000 -sTCP:LISTEN -t | xargs -r kill -9 || echo 'no listener on 3000'
# Windows
for /f "tokens=5" %a in ('netstat -ano ^| findstr :3000 ^| findstr LISTENING') do taskkill /PID %a /F
- Validate recovery:
- Re-run
node -vto confirm version. - Run
npm startand curl the endpoint. - Confirm a listener is present with
lsofornetstat.
Operations Checklist
Use this short runbook to keep sessions fast and predictable.
Pre-flight (1 minute):
nvm use 'lts/*' && node -v && npm -v- Confirm lab root:
pwd(orGet-Location) shows your lab directory. - Check ports 3000 and 3001 are free.
Session steps:
- Choose a project folder (
01-hello-http,02-express-api, or03-file-io-and-env). - Run
npm ciif you changed dependencies; otherwisenpm installonce per new clone. - Start the app:
npm start. - Verify with
curland confirm logs show expected lines.
Diagnostics (if needed):
- Enable inspector:
node --inspect index.jsor equivalent entry file. - Check listeners:
lsof -i :PORT(Unix) or Windowsnetstat. - Read the last 20 log lines and isolate the failing path.
Cleanup:
- Stop the process with Ctrl+C.
- Free ports if processes linger.
- Commit or copy working snapshots for easy rollback next time (optional).
Conclusion
You now have a safe, repeatable Node.js lab on your local machine: fixed versions via nvm, a clean directory layout, and three practical projects that demonstrate core HTTP, basic routing, and file I/O with environment variables. You can verify behavior with simple commands, diagnose common issues quickly, and recover to a known-good state in minutes.
Next steps:
- Keep the pilot narrow and measurable: extend one example at a time and add a simple test per project.
- Add a new project folder when exploring a distinct concern (for example, a cache client or a validation library) so experiments remain isolated.
- Capture each session's versions and outcomes in a NOTES.md file in the project folder to make your results easy to replicate later.