A working JavaScript environment has three parts: a runtime (Node.js), an editor (VS Code), and a browser inspector (Chrome DevTools). Install them in that order, wire them together with one launch.json file, and you can set breakpoints in server and client code from a single window.
Quick Takeaways
- Install Node.js through a version manager (nvm, fnm, or Volta), not the system package manager. You get per-project versions and no
sudo npm. - Use an LTS release for anything you ship. Check the status column at nodejs.org/en/about/previous-releases, because the schedule changes every October.
- VS Code ships with the JavaScript Debugger built in. The old “Debugger for Chrome” extension is obsolete.
- Add ESLint and Prettier on day one. A formatter that runs on save removes style arguments for good.
| Tool | Role | Install method | Verify with |
|---|---|---|---|
| Node.js | Runtime and npm | nvm / fnm / Volta | node -v |
| VS Code | Editor and debugger | Official installer | code --version |
| Chrome DevTools | Browser inspection | Built into Chrome | F12 or Ctrl+Shift+I |
Step 1: Install Node.js with a Version Manager
Node.js runs JavaScript outside the browser. Installing it through a version manager lets you switch versions per project, which matters when one repo needs Node 22 and another needs 24.
Node follows a predictable cycle. Each new major version enters Current for six months, then is promoted to LTS (Long-Term Support) with about 30 months of security fixes. Node.js 24 (“Krypton”) and 22 (“Jod”) are LTS lines, and the project’s own schedule slates Node.js 26 for LTS promotion in October 2026. Starting with Node 27, the project moves to one major release per year, and every release becomes LTS. Production apps should run only Active or Maintenance LTS.
Install on macOS or Linux with nvm
# Install nvm using the command from github.com/nvm-sh/nvm (check the current version tag).
# Then restart your shell and install the latest LTS release:
nvm install --lts
# Make it the default for new terminals
nvm alias default 'lts/*'
# Confirm the install
node -v
npm -v
node -v prints the runtime version (for example v24.x.x). npm -v prints the bundled package manager version. If either command fails, the shell has not loaded nvm. Open a new terminal and retry.
Install on Windows
Use nvm-windows, fnm, or Volta. Alternatively, run the official installer from nodejs.org and accept the defaults, which add Node to your PATH.
# With winget (built into current Windows releases)
winget install Schniz.fnm
fnm install --lts
fnm default lts-latest
node -v
Pin the version per project
Create a .nvmrc file so every teammate (and your CI) uses the same runtime.
# Write the current version into .nvmrc
node -v > .nvmrc
# Later, in the project folder:
nvm use
nvm use reads .nvmrc and switches the active version for that shell. fnm and Volta read the same file or a volta key in package.json.
Version manager comparison
| Manager | Platforms | Switch speed | Auto-switch per folder | Best for |
|---|---|---|---|---|
| nvm | macOS, Linux | Slower (shell script) | Manual or hook | Most tutorials, widest docs |
| nvm-windows | Windows | Moderate | No | Windows-only teams |
| fnm | All | Fast (Rust) | Yes, with a flag | Daily use across platforms |
| Volta | All | Fast | Yes, via package.json | Teams that want pinned toolchains |
Step 2: Install and Configure VS Code
Download VS Code from code.visualstudio.com. On first launch, enable Settings Sync so extensions and preferences follow your account.
Run this once so you can open projects from a terminal:
# macOS/Linux/Windows: open the folder in VS Code
code .
On macOS, run Shell Command: Install ‘code’ command in PATH from the Command Palette (Cmd+Shift+P) if code is not found.
Extensions worth installing
| Extension | Purpose | Required? |
|---|---|---|
| ESLint | Catches bugs and enforces rules | Yes |
| Prettier – Code formatter | Formats on save | Yes |
| Error Lens | Shows diagnostics inline | Optional |
| Path Intellisense | Autocompletes file paths in imports | Optional |
| Live Server | Hot-reloads static HTML | Optional |
Skip extensions that duplicate built-in features. VS Code already provides IntelliSense, Emmet, Git integration, and a debugger for JavaScript.
Workspace settings
Save team-wide settings in .vscode/settings.json so they travel with the repo.
{
// Format with Prettier whenever you save
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
// Run ESLint auto-fixes on save
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
// Use the workspace's TypeScript version for JS type checking
"js/ts.implicitProjectConfig.checkJs": true,
// Keep node_modules out of search results
"search.exclude": {
"**/node_modules": true,
"**/dist": true
}
}
Saving a file now triggers Prettier, then ESLint fixes. Setting checkJs gives you type-error squiggles in plain .js files without adding TypeScript to the build.
Step 3: Initialize a Project
Create a project folder and generate a package.json.
mkdir js-dev-setup && cd js-dev-setup
npm init -y
npm install --save-dev eslint @eslint/js prettier
Then edit package.json to enable ES modules and add scripts:
{
"name": "js-dev-setup",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "node --watch src/server.js",
"start": "node src/server.js",
"lint": "eslint .",
"format": "prettier --write .",
"test": "node --test"
},
"engines": {
"node": ">=22"
}
}
Setting "type": "module" makes Node treat .js files as ES modules, so import and export work without a build step. The –watch flag restarts the process on file changes, which replaces nodemon for most projects. node –test runs the built-in test runner.
A minimal server to debug
// src/server.js
import { createServer } from 'node:http';
const PORT = process.env.PORT ?? 3000;
// Pure function: easy to unit test and to step through in a debugger
export function greet(name = 'world') {
return `Hello, ${name}!`;
}
const server = createServer((req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const message = greet(url.searchParams.get('name') ?? undefined);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message }));
});
server.listen(PORT, () => {
console.log(`Listening on http://localhost:${PORT}`);
});
Run npm run dev and visit http://localhost:3000/?name=Ada. The server returns {"message":"Hello, Ada!"}. Edit the file and save, and the process restarts on its own.
ESLint flat config
ESLint 9 and later use eslint.config.js. Create it in the project root:
// eslint.config.js
import js from '@eslint/js';
export default [
js.configs.recommended,
{
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
// Declare the Node.js globals your code uses
globals: {
process: 'readonly',
console: 'readonly',
URL: 'readonly'
}
},
rules: {
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
eqeqeq: 'error',
'prefer-const': 'error'
}
}
];
Run npm run lint. Without the globals block, ESLint reports process as undefined.
Step 4: Debug Node.js in VS Code
The built-in JavaScript Debugger (js-debug) handles Node.js, Chrome, and Edge. You configure it in .vscode/launch.json.
{
"version": "0.2.0",
"configurations": [
{
// Run the current file under the debugger
"name": "Node: Current File",
"type": "node",
"request": "launch",
"program": "${file}",
"skipFiles": ["<node_internals>/**"]
},
{
// Launch the dev server with breakpoints enabled
"name": "Node: Server",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/src/server.js",
"envFile": "${workspaceFolder}/.env",
"console": "integratedTerminal",
"skipFiles": ["<node_internals>/**"]
},
{
// Attach to a process started with --inspect
"name": "Node: Attach",
"type": "node",
"request": "attach",
"port": 9229,
"restart": true
}
]
}
Click the gutter beside a line in src/server.js to set a breakpoint, select Node: Server, and press F5. Execution pauses on the breakpoint. The Variables pane shows local state, and the Debug Console evaluates expressions in the paused scope.
skipFiles keeps the debugger out of Node’s internal modules, so F11 (step into) stays inside your code. The Attach configuration helps when a script runs elsewhere, such as in Docker:
# Start any script with the inspector open
node --inspect=9229 src/server.js
For quick one-offs, run Debug: Toggle Auto Attach from the Command Palette. Any Node process you start in the integrated terminal then connects to the debugger automatically.
Step 5: Debug in the Browser with Chrome DevTools
Open DevTools with F12, Ctrl+Shift+I (Windows/Linux), or Cmd+Option+I (macOS). Four panels cover most daily work.
| Panel | Use it to | Shortcut or tip |
|---|---|---|
| Elements | Inspect and edit the live DOM and CSS | Right-click, then Inspect |
| Console | Run JavaScript, read logs and errors | Use $0 for the selected element |
| Sources | Set breakpoints, step through code | Ctrl+P opens any file |
| Network | Inspect requests, timing, payloads | Filter by Fetch/XHR |
| Performance | Record and profile runtime behavior | Record, reload, stop |
Console methods beyond console.log()
const users = [
{ id: 1, name: 'Ada', role: 'admin' },
{ id: 2, name: 'Linus', role: 'user' }
];
console.table(users); // Renders an array of objects as a table
console.group('Request'); // Collapsible group
console.time('fetch'); // Start a timer
await fetch('/api/users');
console.timeEnd('fetch'); // Prints elapsed milliseconds
console.groupEnd();
console.assert(users.length === 3, 'Expected 3 users'); // Logs only when false
console.table() prints a sortable grid. console.assert() stays silent unless the condition is false, so you can leave it in during development without noise.
Breakpoint types in the Sources panel
| Type | How to add | Use case |
|---|---|---|
| Line | Click the line number | Pause at a known spot |
| Conditional | Right-click, then Add conditional breakpoint | Pause only when user.id === 42 |
| Logpoint | Right-click, then Add logpoint | Log a value without editing code |
| DOM change | Elements panel, right-click node, Break on | Find what mutates an element |
| XHR/Fetch | Sources, XHR/fetch Breakpoints | Pause on a matching URL |
Logpoints replace most temporary console.log() calls. You never commit them by accident, because they live in the browser, not in the file.
Debug client code from VS Code
Add a Chrome configuration to launch.json to set breakpoints in VS Code and run the page in a debugging Chrome instance:
{
"name": "Chrome: Localhost",
"type": "chrome",
"request": "launch",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}/src"
}
Start your dev server first, then press F5. The webRoot setting maps browser URLs to local files, which is how source maps connect bundled output to your original source. For a full-stack project, use a compound configuration to start the server and the browser together.
Bad Code vs. Good Code: Setup Anti-Patterns
Anti-pattern 1: Debugging with scattered logs
// Bad: logs left in, no structure, impossible to filter
function applyDiscount(cart, code) {
console.log('cart', cart);
console.log('code', code);
const total = cart.items.reduce((s, i) => s + i.price, 0);
console.log('total', total);
return total * (code == 'SAVE10' ? 0.9 : 1);
}
// Good: strict equality, named constants, no debug output.
// Inspect values with a breakpoint or a logpoint instead.
const DISCOUNTS = { SAVE10: 0.1 };
export function applyDiscount(cart, code) {
const subtotal = cart.items.reduce((sum, item) => sum + item.price, 0);
const rate = DISCOUNTS[code] ?? 0;
return subtotal * (1 - rate);
}
The refactor removes the loose == comparison, drops the magic string from the logic, and keeps the function pure. ESLint’s eqeqeq rule flags the first version on save.
Anti-pattern 2: Global installs and sudo npm
# Bad: global tool versions drift between machines, and sudo breaks file ownership
sudo npm install -g eslint prettier
# Good: project-local tools pinned in package.json, run through npm scripts
npm install --save-dev eslint prettier
npx eslint .
Local installs make npm ci reproduce the exact toolchain on any machine or CI runner.
Anti-pattern 3: Hardcoded secrets
// Bad: the key is now in Git history
const apiKey = 'sk_live_abc123';
// Good: read from the environment; load .env during development
const apiKey = process.env.API_KEY;
if (!apiKey) throw new Error('API_KEY is not set');
# Node 20.6+ loads a .env file natively
node --env-file=.env src/server.js
Add .env to .gitignore. The thrown error makes a missing variable fail fast at startup instead of deep inside a request.
Troubleshooting Common Setup Problems
| Symptom | Likely cause | Fix |
|---|---|---|
command not found: node |
Shell has not loaded nvm/fnm | Open a new terminal; check your shell profile |
EACCES during npm install -g |
Global dir owned by root | Use a version manager; never sudo npm |
| Breakpoints show as gray circles | Source maps or webRoot wrong |
Fix webRoot; confirm sourceMap: true in the bundler |
Cannot use import statement outside a module |
Missing module type | Add "type": "module" or rename to .mjs |
ESLint says process is not defined |
Missing globals in config | Add Node globals to languageOptions.globals |
| Debugger exits immediately | Wrong program path |
Use ${workspaceFolder} and check the file exists |
Recommended Project Structure
js-dev-setup/
├── .vscode/
│ ├── launch.json
│ └── settings.json
├── src/
│ └── server.js
├── test/
│ └── server.test.js
├── .gitignore
├── .nvmrc
├── eslint.config.js
├── package.json
└── README.md
Commit .vscode/launch.json and settings.json so new contributors press F5 and get the same experience on day one. Keep personal preferences, such as themes and font size, in your user settings.
FAQ
Which Node.js version should I install for development?
Install the latest LTS release. LTS lines get about 30 months of security fixes and are the only releases the Node.js project recommends for production. Check the status column on nodejs.org before you install, since a Current release moves to LTS each October.
Do I need to install npm separately from Node.js?
No. npm ships with Node.js. Running node -v and npm -v after installation confirms both. To use another package manager such as pnpm or Yarn, enable it with corepack enable, which ships with Node.
Is the Debugger for Chrome extension still needed in VS Code?
No. VS Code bundles the JavaScript Debugger, which supports Node.js, Chrome, and Edge. Use "type": "node" for server code and "type": "chrome" for browser code in launch.json.
Should I use VS Code’s debugger or Chrome DevTools?
Use both. Chrome DevTools gives the best view of the DOM, network, and rendering performance. VS Code is better for server code and for stepping through source files without leaving your editor. A compound launch configuration lets you debug both sides in one session.




