Home JavaScript npm and package.json — EACCES Permission Error Without sudo
Beginner 9 min · March 05, 2026
npm and package.json Explained

npm and package.json — EACCES Permission Error Without sudo

The EACCES error when running npm install -g is caused by root ownership from sudo.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.

Follow
Production
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 20 min
  • Basic programming fundamentals
  • A computer with internet access
  • Willingness to follow along with examples
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • npm is the Node Package Manager — a registry, CLI, and file format combined
  • package.json is your project's manifest: name, version, dependencies, scripts
  • dependencies are for runtime; devDependencies are for development tools
  • npm install downloads packages into node_modules and records them in package.json
  • Semantic versioning (^1.4.2) controls which updates are safe to accept
  • The package-lock.json locks every sub-dependency version — never delete it
✦ Definition~90s read
What is npm and package.json?

npm (Node Package Manager) is the default package manager for Node.js, created in 2010 to solve the problem of sharing and reusing JavaScript code across projects. Before npm, JavaScript developers manually downloaded libraries, copied them into their projects, and managed versions by hand — a fragile, error-prone process. npm introduced a centralized registry (now hosting over 2 million packages) and a declarative dependency model via package.json, letting you specify exactly which versions of which packages your project needs.

Imagine you're baking a cake.

It also handles transitive dependencies (dependencies of your dependencies) automatically, which is both a blessing and a curse — it saves enormous time but can balloon your node_modules folder to hundreds of megabytes. Alternatives like Yarn and pnpm exist, offering faster installs or disk-space deduplication, but npm remains the default and most widely used.

You should use npm for any Node.js project, but if you're building a frontend-only app, tools like npm scripts or bundlers (Webpack, Vite) often replace the need for a full build pipeline.

The package.json file is the heart of any Node.js project — it's a JSON manifest that declares your project's name, version, entry point, scripts, and most importantly, its dependencies. When you run npm init, npm walks you through creating this file, which acts as your project's identity card.

The dependencies field lists packages your app needs at runtime (like Express or React), while devDependencies are tools only needed during development (like testing frameworks or build tools). This distinction matters because when you deploy to production, you can run npm install --production to skip dev dependencies, keeping your deployment lean.

The package-lock.json file, auto-generated by npm, locks down the exact version of every installed package and its transitive dependencies — this ensures that every developer and every deployment gets the exact same dependency tree, preventing the classic "works on my machine" problem.

When you run npm install <package>, npm downloads the package from the registry and places it into a node_modules folder in your project root. This folder can grow to thousands of files — a typical React project might have 50,000+ files in node_modules.

The package-lock.json file tracks the exact version and integrity hash of every installed package, so you can commit it to version control and guarantee reproducible builds. If you ever delete node_modules (which is safe to do), running npm install will rebuild it exactly as before, thanks to the lock file.

The EACCES permission error you might encounter when running npm commands without sudo is a common symptom of installing Node.js via system package managers (like apt on Linux) which place npm's global directory in a system-owned location. The fix is either to reconfigure npm's prefix to a user-owned directory or use a version manager like nvm, which avoids the permission issue entirely by installing Node.js and npm in your home directory.

Plain-English First

Imagine you're baking a cake. Instead of growing your own flour, sugar, and eggs from scratch, you drive to a supermarket and grab exactly what you need. npm is that supermarket — a giant store of reusable code written by other developers. package.json is your shopping list, recording every ingredient your project needs so anyone else (or a server) can recreate your exact setup in seconds.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

Every professional JavaScript project you'll ever work on uses npm. It powers over 2 million packages and is downloaded billions of times every week. When you land a junior dev role and someone says 'just run npm install', you need to know exactly what's happening under the hood — not just how to type the command.

Before npm existed, sharing code between projects was painful. You'd copy-paste files manually, lose track of versions, and when a bug fix came out you'd have no way of knowing. npm solves all of that by giving every project a single source of truth: the package.json file. It tracks what external code your project depends on, what version of it you need, and even how to run your app.

By the end of this article you'll be able to create a project from scratch, install packages, understand every key field in package.json, and explain the difference between a dependency and a devDependency — the question that trips up a surprising number of candidates in junior dev interviews.

What npm Actually Is and Why It Was Built

npm stands for Node Package Manager. It ships automatically when you install Node.js, so if Node is on your machine, npm is already there.

The core job of npm is simple: let developers share and reuse code. That reusable chunk of code is called a 'package'. A package could be anything — a tool to format dates, a full web framework, a utility to send emails. Instead of writing all of that yourself, you pull in someone else's tested, maintained package.

npm has three parts that work together. First, there's the registry — a huge online database at registry.npmjs.org where packages live. Second, there's the CLI (command-line interface) — the npm commands you type in your terminal. Third, there's the website (npmjs.com) where you can search for and read about packages.

Think of the registry as the supermarket's warehouse, the CLI as your shopping trolley, and npmjs.com as the store's website where you browse before you go. You don't need to think about all three at once — most of the time you're just typing commands in the terminal and npm handles the rest.

check-npm-installed.shBASH
1
2
3
4
5
6
7
8
9
10
# First, verify that Node.js is installed on your machine
# This prints the version of Node you have
node --version

# Now verify npm is installed — it comes bundled with Node
# This prints your npm version
npm --version

# If you see version numbers printed, you're good to go.
# If you see 'command not found', visit nodejs.org and install Node first.
Output
v20.11.0
10.2.4
💡Pro Tip:
You never install npm on its own. Install Node.js from nodejs.org and npm comes with it automatically. If your npm is out of date, run 'npm install -g npm@latest' to update it.
📊 Production Insight
npm v7 changed default peer dependency handling, causing CI failures for older projects using legacy peer deps.
Lockfile conflicts in Git are the #1 source of "works on my machine" bugs — always commit package-lock.json.
Don't use sudo with npm. Configure a user-level prefix or use nvm.
🎯 Key Takeaway
npm is the standard package manager for Node.js.
It comes bundled with Node — no separate install needed.
Always update npm before starting a new project: 'npm install -g npm@latest'.
npm-package-json-explained npm Permission Architecture Layers Hierarchical components involved in npm global installs and permissions User Environment Shell Profile | PATH Variable | Home Directory npm Configuration npmrc File | Prefix Setting | Cache Directory Global Installation Path ~/.npm-global/bin | ~/.npm-global/lib | ~/.npm-global/node_modules Package Management npm CLI | package.json | node_modules System Permissions User Ownership | File Mode | sudo Access THECODEFORGE.IO
thecodeforge.io
Npm Package Json Explained

Creating Your First package.json — The Project's Identity Card

Every npm project needs a package.json file sitting at its root. This file is the heart of your project. It records your project's name, version, which packages it depends on, and useful scripts for running or testing your app. Without it, npm has no idea what your project needs.

You create it by running 'npm init' inside your project folder. npm will ask you a series of questions — project name, version, description, entry point, and so on. If you just want sensible defaults and don't want to answer every question, use 'npm init -y' (the -y flag means 'yes to everything').

The resulting file is plain JSON — just text. You can open it in any code editor and edit it by hand. That's one of the things beginners find surprising: it's not magic, it's just a config file you can read and change.

Every field in package.json has a specific meaning. The two most important are 'name' (what your package is called, lowercase, no spaces) and 'version' (which follows a three-number system called Semantic Versioning, or semver, like 1.0.0). We'll look at what those numbers mean in a moment.

create-project.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
# Step 1: Create a new folder for your project
mkdir my-first-node-project

# Step 2: Move into that folder
cd my-first-node-project

# Step 3: Create a package.json with all default values
# The -y flag skips the questionnaire and accepts all defaults
npm init -y

# Step 4: Look at what was created
cat package.json
Output
Wrote to /Users/you/my-first-node-project/package.json:
{
"name": "my-first-node-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
🔥What is 'main'?
The 'main' field tells Node.js which file is the entry point of your application — the first file it should run. By convention this is index.js. If someone installs your code as a package themselves, this is also the file that gets loaded when they 'require' your package.
📊 Production Insight
Forgetting 'license' in package.json can block corporate npm registry publishing.
The 'name' field must be unique on npm — choose a scoped name (@your-scope/package) for internal packages.
Setting 'private': true prevents accidental publishing to public registry.
🎯 Key Takeaway
Use 'npm init -y' for a quick start.
Edit package.json by hand — it's just JSON.
Add 'private': true for internal projects to avoid accidental publish.

Installing Packages — Dependencies vs devDependencies

Now for the fun part: pulling in other people's code. When you run 'npm install ', npm downloads that package into a folder called node_modules and automatically records it in your package.json under a field called 'dependencies'.

But not all packages are equal. Some packages your app needs to actually run in production — like express (a web server) or axios (for making HTTP requests). These go in 'dependencies'. Other packages are only needed while you're developing — like jest (a testing tool) or eslint (a code quality checker). Your end users will never need these. These go in 'devDependencies'.

To install a production dependency: 'npm install axios' To install a dev-only dependency: 'npm install jest --save-dev'

The '--save-dev' flag is the difference. Get this right and your production deployments only install what they actually need, which makes them faster and leaner.

Semantic versioning controls which version gets installed. The version number '1.4.2' means: major version 1, minor version 4, patch 2. A caret symbol (^1.4.2) means 'install 1.4.2 or any compatible minor/patch update'. A tilde (~1.4.2) means 'install 1.4.2 or any patch update only'. Most of the time you'll see the caret and that's fine for beginners.

index.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// First, in your terminal run:
// npm install axios
// npm install chalk --save-dev

// Now let's use axios — a package for making HTTP requests
// This is a PRODUCTION dependency (your app needs it to run)
const axios = require('axios');

// Fetch a public post from a free test API
async function fetchBlogPost(postId) {
  try {
    // axios.get sends an HTTP GET request to the URL
    const response = await axios.get(
      `https://jsonplaceholder.typicode.com/posts/${postId}`
    );

    // response.data contains the actual JSON returned by the API
    const post = response.data;

    console.log('Post Title:', post.title);
    console.log('Post Body:', post.body);
  } catch (error) {
    // If the request fails, catch and print the error
    console.error('Failed to fetch post:', error.message);
  }
}

// Fetch the first blog post
fetchBlogPost(1);
Output
Post Title: sunt aut facere repellat provident occaecati excepturi optio reprehenderit
Post Body: quia et suscipit
suscipit recusandae consequuntur expedita et cum
reprehenderit molestiae ut ut quas totam
nostrum rerum est autem sunt rem eveniet architecto
Try it live
⚠ Watch Out:
Never commit the node_modules folder to Git. It can contain hundreds of thousands of files. Instead, add node_modules to your .gitignore file. Anyone who clones your repo just runs 'npm install' and npm recreates the whole folder from your package.json automatically.
📊 Production Insight
Installing everything as a dependency bloats production builds — a tool like Webpack that's only used at build time should be devDependency.
npm v7+ requires explicit --save-dev for dev dependencies; npm v5-v6 automatically saved to dependencies if you didn't use the flag.
Always verify your dependencies with 'npm ls --prod' before deployment.
🎯 Key Takeaway
Use '--save-dev' for development-only tools.
Caret (^) allows minor updates; tilde (~) allows patch only.
Run 'npm install --production' in CI to install only runtime deps.
npm-package-json-explained THECODEFORGE.IO npm Permission Architecture Layers How npm interacts with OS and user permissions User Shell Bash/Zsh | PATH variable | Environment config npm CLI Global installs | Local installs | Config settings Node.js Runtime Module resolution | require() paths | node_modules File System /usr/local/lib/node_modules | ~/.npm-global | Project node_modules OS Permissions User ownership | Group access | sudo escalation THECODEFORGE.IO
thecodeforge.io
Npm Package Json Explained

npm Scripts — Your Project's Command Centre

The 'scripts' field in package.json is one of the most underappreciated features for beginners. It lets you define shortcut commands for your project that anyone on the team can run without knowing the underlying tool or its exact flags.

For example, instead of everyone having to remember 'node --watch src/index.js', you define a 'start' script once and everyone just types 'npm start'. Instead of 'jest --coverage --verbose', you write a 'test' script.

There are two special script names: 'start' and 'test'. These run with 'npm start' and 'npm test'. Any other custom script name you invent runs with 'npm run <script-name>' — note the extra 'run' keyword.

Scripts can also chain commands together using '&&' (run second command only if first succeeds) or '&' (run both simultaneously). This is how teams build, lint, and test their code in one command. It's also how CI/CD pipelines (automated deployment tools) know how to build your project on a server.

Think of scripts as the user manual for your project. A new developer clones your repo, reads the scripts section, and immediately knows how to start, test, build, and deploy the app — without asking you.

package.jsonJSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
  "name": "my-first-node-project",
  "version": "1.0.0",
  "description": "A demo project to learn npm and package.json",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",

    "dev": "node --watch index.js",

    "test": "jest --coverage",

    "lint": "eslint src/**/*.js",

    "build:and:lint": "npm run lint && node index.js"
  },
  "dependencies": {
    "axios": "^1.6.7"
  },
  "devDependencies": {
    "chalk": "^5.3.0",
    "eslint": "^8.56.0",
    "jest": "^29.7.0"
  },
  "keywords": ["demo", "nodejs", "npm"],
  "author": "Your Name <you@example.com>",
  "license": "MIT"
}
Output
# Running: npm start
# Executes: node index.js
# Output: whatever your index.js prints
# Running: npm run dev
# Executes: node --watch index.js
# Node watches for file changes and auto-restarts — great for development
# Running: npm test
# Executes: jest --coverage
# Jest finds and runs all test files and prints a coverage report
💡Interview Gold:
Interviewers love asking 'what's the difference between npm start and npm run start?' The answer: there is none — both work. 'start' and 'test' are blessed shortcuts that don't need the 'run' keyword. Every other custom script requires 'npm run <name>'.
📊 Production Insight
Using 'npm run' without pre/post hooks can miss validation steps — add 'prebuild' and 'postbuild' scripts for consistency.
CI environments expect scripts like 'test', 'build', 'lint' — standardize them across projects.
Scripts that fail silently (no exit code) break CI — always ensure commands return proper exit codes.
🎯 Key Takeaway
'npm start' and 'npm test' are special shortcuts.
Use 'pretest', 'posttest' hooks for setup/teardown.
Document scripts clearly — they're the team's playbook.

Understanding node_modules and package-lock.json

When you run npm install, npm downloads every package and its own dependencies (sub-dependencies) into a folder called node_modules. This folder is the runtime environment for your project. It's also massive — a typical React project can have 200,000+ files in node_modules. That's why you never commit it to Git.

Instead, npm created package-lock.json. This file records the exact version of every single package and every sub-package that was installed. It's a complete snapshot of your dependency tree. When someone else clones your repo and runs npm install, npm reads the lockfile and installs the exact same versions, down to the sub-dependency. This eliminates "works on my machine" problems.

Without package-lock.json, if package 'A' depends on package 'B' with range ^1.0.0, and 'B' releases version 1.1.0 after you last ran install, your teammate gets B@1.1.0 while you have B@1.0.0. The lockfile locks everything.

You should ALWAYS commit package-lock.json to Git. Never delete it, never add it to .gitignore. Some old tutorials say to delete it because it's "magical" — that's terrible advice. Keep it.

package-lock.json (excerpt)JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
  "name": "my-first-node-project",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "my-first-node-project",
      "dependencies": {
        "axios": "^1.6.7"
      }
    },
    "node_modules/axios": {
      "version": "1.6.7",
      "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
      "integrity": "sha512-...",
      "dependencies": {
        "follow-redirects": "^1.15.4"
      }
    },
    "node_modules/follow-redirects": {
      "version": "1.15.5",
      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
      "integrity": "sha512-..."
    }
  }
}
⚠ Critical:
Do NOT add package-lock.json to .gitignore. This file is essential for reproducible builds. If you accidentally ignored it, remove it from .gitignore and commit the lockfile immediately.
📊 Production Insight
Merge conflicts in package-lock.json are common — never manually edit it. Accept theirs, then run npm install to regenerate correctly.
Different npm versions (v6 vs v7+) produce different lockfile formats. Ensure all developers use the same npm version, or commit using the lowest version.
npm v7+ uses lockfileVersion 2; npm v6 uses lockfileVersion 1. Upgrading npm changes the lockfile format — that's fine.
🎯 Key Takeaway
Always commit package-lock.json.
Never delete it or ignore it.
For merge conflicts, take theirs then run 'npm install' to regenerate.
Should I Commit package-lock.json?
IfI'm writing an npm package (library)
UseYes, commit the lockfile — it's recommended for libraries too, though some publishers choose not to. At minimum, commit it for your development environment.
IfI'm writing an application (app or service)
UseAbsolutely yes — always commit it. Reproducible builds depend on it.
IfMy team uses yarn or pnpm instead of npm
UseCommit the equivalent lock file (yarn.lock or pnpm-lock.yaml). Never commit both package-lock.json and yarn.lock — pick one package manager.

Semantic Versioning: The Contract Your package.json Signs

Every dependency in your package.json carries a version string like ^4.17.21. That caret isn't decorative. It's a promise. Semantic versioning (semver) is the unspoken contract between you and every package author. The format is MAJOR.MINOR.PATCH. Bump the major for breaking changes. Minor for new features (backward-compatible). Patch for bug fixes. The caret (^) locks the major version but allows minor and patch updates. The tilde (~) locks the major AND minor, only permitting patches. No symbol means a deadlock — exact version only. In production, you never want to pin exact versions for everything (fork management nightmare). But you also never want * (any version). That's chaos. Practically: use ^ for most deps, ~ for tools like formatters, and exact for deployment-related packages. Your CI pipeline will thank you. Understanding semver in your package.json saves you from the dreaded "works on my machine" syndrome across environments.

package.jsonJSON
1
2
3
4
5
6
7
{
  "dependencies": {
    "express": "^4.18.2",  // allows 4.x.x updates
    "lodash": "~4.17.21", // only 4.17.x patches
    "left-pad": "1.3.0"    // exact, never changes
  }
}
Output
npm install express@4.18.2
+ express@4.18.2
npm notice created a lockfile as package-lock.json
npm WARN deprecated: some deprecation warnings may appear
⚠ Production Trap:
Never commit package-lock.json changes blindly. A single minor bump can silently break your API contract. Always review lockfile diffs in PRs. I've seen ^0.x.x packages jump major versions because maintainers forgot semver rules.
🎯 Key Takeaway
Your package.json is a legal document with npm. Semver is the fine print — read it before you sign.

Peer Dependencies: The Ticking Time Bomb in Your package.json

Most developers understand dependencies and devDependencies. Few grasp peerDependencies. This field says: "I need this package to work, but you — the consumer — must provide it." Think React plugins. If you build a UI component library, you shouldn't bundle React inside it. Your consumers already have React. Bundling duplicates it (and breaks hooks). That's where peerDependencies shine. They declare compatibility without installation. But here's the trap: npm v6 and earlier installed peer deps automatically. npm v7+ does not. Your library installs silently, then explodes at runtime with Cannot find module 'react'. The fix? Specify peerDependenciesMeta to mark optional peers. And always test your library in a fresh project. Another pattern: use peerDependencies for plugins (Webpack loaders, ESLint configs, Babel presets). They declare a shared interface. Misuse them, and you get version collisions that require --legacy-peer-deps to resolve. That flag is a code smell.

package.jsonJSON
1
2
3
4
5
6
7
8
9
10
11
12
13
{
  "name": "my-react-grid",
  "version": "2.0.0",
  "peerDependencies": {
    "react": ">=16.8.0",
    "react-dom": ">=16.8.0"
  },
  "peerDependenciesMeta": {
    "react-dom": {
      "optional": true  // for server-side rendering
    }
  }
}
Output
npm install my-react-grid
+ my-react-grid@2.0.0
npm WARN my-react-grid@2.0.0 requires a peer of react@>=16.8.0 but none is installed.
🔥Pro Insight:
If your library has peerDependencies, document the exact versions you've tested. I once shipped a charting library that declared react@^17, but a consumer used React 18. The new concurrent features broke our lifecycle hooks silently.
🎯 Key Takeaway
Peer dependencies are promises to your consumers. Break them, and your library becomes tech debt overnight.

The `exports` Field: Modern package.json's Best Kept Secret

For years, the main field was the only entry point. Then came browser, module, and a rat's nest of bundler-specific fields. The exports field in Node.js 12.7+ cleaned it all up. It defines a single source of truth for module resolution. Specify one entry point, or map multiple (like import vs require). It also enables subpath exports — exposing internal modules without leaking your entire src folder. This is huge for library authors. Without exports, consumers can require('your-package/internal/secret') and break encapsulation. With exports, only explicitly listed paths are accessible. The catch? It's strict. If you define exports, the main field is ignored. Misconfigure it, and your library becomes unimportable. Always test both require() and import syntax. A pattern I use: expose a clean API surface, hide implementation details, and provide a package.json conditional export for different environments (Node vs browser vs React Native).

package.jsonJSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
  "name": "my-utils",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    },
    "./date": {
      "import": "./dist/date.mjs",
      "require": "./dist/date.cjs"
    }
  },
  "main": "./dist/index.cjs",  // fallback for older Node
  "type": "module"
}
Output
// Consumer can do:
import { format } from 'my-utils'; // works
import { parse } from 'my-utils/date'; // works
import { secret } from 'my-utils/internal'; // ERR_PACKAGE_PATH_NOT_EXPORTED
⚠ Common Pitfall:
When adding exports, test immediately with both node --experimental-modules and a bundler. If you forget the "main" fallback, TypeScript users on older Node versions will get cryptic resolution errors.
🎯 Key Takeaway
The exports field is your package's firewall. Use it to enforce public API boundaries and kill hidden dependencies.

The Fields Nobody Reads: browser, private, engines, homepage, bugs, files

Your package.json is cluttered with metadata fields that most developers skip. But ignoring them is a mistake. The browser field tells bundlers like webpack or Rollup which module to use in browser environments. If you maintain a library that works both server-side and client-side, this field prevents bundlers from pulling in Node.js-specific code. The private field is a safety switch: set it to true to prevent accidental npm publish. This is mandatory for internal projects. The engines field declares which Node.js and npm versions your package supports. It's advisory, not enforced, but tools like engine-strict config or yarn can fail the install if the host doesn't match. The homepage and bugs fields are for users: homepage should point to your project's website or repo, and bugs to an issue tracker. Without them, users have to hunt for support. The files field whitelists which files are included when your package is published. By default, npm includes everything except .gitignore-listed files. Use files to ship only what's needed: "files": ["dist", "README.md"]. This reduces install size and avoids leaking tests or source maps. Pro tip: combine files with .npmignore for fine-grained control. These fields are small but critical for maintainability and user experience.

package.jsonJSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
  "name": "my-library",
  "version": "1.0.0",
  "private": true,
  "engines": {
    "node": ">=14.0.0",
    "npm": ">=6.0.0"
  },
  "browser": {
    "./index.js": "./browser.js"
  },
  "homepage": "https://github.com/user/my-library#readme",
  "bugs": {
    "url": "https://github.com/user/my-library/issues"
  },
  "files": [
    "dist",
    "README.md"
  ]
}
⚠ Don't Forget private: true
If your project is internal, set private: true to prevent accidental publication. One npm publish mistake can leak proprietary code.
📊 Production Insight
In CI, enforce engines with npm config set engine-strict true to fail builds on incompatible Node versions.
🎯 Key Takeaway
Metadata fields like browser, private, engines, homepage, bugs, and files are not optional—they define your package's contract with users and tools.

npx vs npm: When to Use Which

npm is the package manager; npx is the package runner. The confusion stems from npx being bundled with npm since version 5.2. Use npm to install packages globally or locally. Use npx to execute a package without installing it globally. For example, npx create-react-app my-app downloads and runs the latest version of create-react-app without polluting your global installs. npx also resolves locally installed binaries: npx eslint . runs the local eslint from node_modules/.bin. This is cleaner than using npm scripts or global installs. Another use case: running a one-off tool like npx http-server to serve a directory. npx checks the local node_modules first, then the npm cache, then downloads if needed. It also supports specifying a version: npx eslint@7 .. The downside: npx adds a slight overhead for downloading if the package isn't cached. For frequently used tools, install them locally and run via npm scripts. Never use npx in production scripts—use npm scripts with local binaries instead. The rule of thumb: install what you depend on, run what you need once.

terminalBASH
1
2
3
4
5
6
7
8
9
10
11
# Install a package locally
npm install eslint --save-dev

# Run it via npx (finds local binary)
npx eslint .

# Run a one-off tool without installing
npx create-react-app my-app

# Specify a version
npx eslint@7 .
💡Prefer npm scripts over npx in CI
In CI, use npm run lint instead of npx eslint to avoid download overhead and ensure deterministic versions.
📊 Production Insight
For team consistency, define all commands in npm scripts and avoid npx in package.json scripts—use the local binary path directly.
🎯 Key Takeaway
Use npm to install, npx to execute. npx is ideal for one-off commands and running local binaries without global installs.

package-lock.json lockfileVersion: What Each Version Means

The lockfileVersion in package-lock.json determines how npm resolves dependencies. It's an integer that has changed across npm versions. Version 1 (npm 5 and 6) uses a flat structure with requires and dependencies fields. It's human-readable but can cause issues with hoisting and duplication. Version 2 (npm 7+) introduced a more deterministic tree structure with packages and dependencies sections. It's stricter about hoisting and includes metadata like link for workspaces. Version 3 (npm 9+) is similar to v2 but drops the legacy dependencies field, relying solely on packages. This reduces file size and complexity. The lockfile version is auto-generated based on your npm version. If you upgrade npm, the lockfile may be upgraded. This can cause merge conflicts in teams with mixed npm versions. To enforce a consistent lockfile version, use an .npmrc file with lockfile-version=2. Or use npm install --lockfile-version=2. In practice, stick with v2 for npm 7+ projects. Avoid committing lockfiles generated by different npm versions—it leads to non-reproducible installs. Always regenerate the lockfile with a consistent npm version in CI.

package-lock.json (excerpt)JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// lockfileVersion 1 (npm 5-6)
{
  "name": "my-app",
  "lockfileVersion": 1,
  "requires": true,
  "dependencies": {
    "lodash": {
      "version": "4.17.21",
      "resolved": "...",
      "integrity": "sha512-..."
    }
  }
}

// lockfileVersion 2 (npm 7-8)
{
  "name": "my-app",
  "lockfileVersion": 2,
  "packages": {
    "node_modules/lodash": {
      "version": "4.17.21",
      "resolved": "...",
      "integrity": "sha512-..."
    }
  },
  "dependencies": {
    "lodash": {
      "version": "4.17.21",
      "resolved": "...",
      "integrity": "sha512-..."
    }
  }
}

// lockfileVersion 3 (npm 9+)
{
  "name": "my-app",
  "lockfileVersion": 3,
  "packages": {
    "node_modules/lodash": {
      "version": "4.17.21",
      "resolved": "...",
      "integrity": "sha512-..."
    }
  }
}
🔥Lockfile Version Matters
Mismatched lockfile versions cause non-reproducible installs. Enforce a version with .npmrc or CI config.
📊 Production Insight
In CI, run npm install --lockfile-version=2 to ensure deterministic installs regardless of local npm version.
🎯 Key Takeaway
lockfileVersion determines dependency resolution strategy. Use v2 for npm 7+ and enforce consistency across your team.
sudo vs Local Prefix for npm Comparing two approaches to handle EACCES errors Using sudo Local Prefix Permission Requirement Requires sudo for global installs No sudo needed; user-owned directory Security Risk High; sudo escalates privileges Low; operates within user space Global Package Location /usr/local/lib/node_modules ~/.npm-global/lib/node_modules PATH Configuration Default system PATH Must add ~/.npm-global/bin to PATH Ease of Setup Immediate but risky Requires one-time config Long-term Maintenance Recurring sudo prompts No permission issues after setup THECODEFORGE.IO
thecodeforge.io
Npm Package Json Explained

Pre/Post Script Hooks: Automate Without Extra Tools

npm scripts support pre and post hooks that run automatically before and after a named script. For example, if you define prebuild, build, and postbuild, running npm run build triggers prebuildbuildpostbuild. This is built-in, no plugins needed. Common uses: lint before test (pretest), build before deploy (predeploy), or clean after build (postbuild). The hook names are derived by prepending pre or post to the script name. They work for any script, including lifecycle scripts like prepublishOnly. Be careful: hooks can chain unexpectedly. If you have pretest and test scripts, and pretest itself has a prepretest hook, it will run. This can lead to confusion. Keep hooks simple and avoid deep nesting. Also, hooks run even if the main script fails? No—if prebuild fails, build and postbuild are skipped. This is useful for validation. For example, a prebuild script that checks for environment variables can prevent a broken build. Use hooks to enforce workflows without external tools like Husky (though Husky is still useful for git hooks).

package.jsonJSON
1
2
3
4
5
6
7
8
9
10
11
{
  "scripts": {
    "prebuild": "echo 'Running prebuild checks'",
    "build": "webpack --mode production",
    "postbuild": "echo 'Build complete, running cleanup'",
    "pretest": "eslint .",
    "test": "jest",
    "predeploy": "npm run build",
    "deploy": "aws s3 sync dist/ s3://my-bucket"
  }
}
⚠ Avoid Deep Hook Chains
Hooks can nest. prebuildpreprebuild → ... Keep it shallow to avoid debugging nightmares.
📊 Production Insight
Use prepublishOnly hook to run tests and build before publishing. It runs before npm publish and can prevent broken releases.
🎯 Key Takeaway
Pre/post hooks automate sequential tasks without extra tools. Use them for validation, cleanup, or chaining steps.
● Production incidentPOST-MORTEMseverity: high

EACCES Permission Error During npm install -g

Symptom
Permission denied errors when installing packages globally. sudo npm install works but changes ownership of node_modules to root, causing permission issues on subsequent pulls.
Assumption
Using sudo is the normal way to fix permission problems.
Root cause
Default npm global install location (/usr/lib/node_modules) requires root access. Running with sudo changes ownership of installed packages to root, so later operations by non-root users fail.
Fix
Configure npm to use a user-owned prefix: npm config set prefix ~/.npm-global, then add export PATH=~/.npm-global/bin:$PATH to your profile. Or use nvm to manage Node versions entirely in user space.
Key lesson
  • Never use sudo with npm. It breaks permissions that cascade across the team.
  • Always configure a user-level prefix for global installs.
  • nvm avoids the problem entirely by isolating Node versions in your home directory.
Production debug guideSymptom → Action guide for the npm errors that waste the most time on a team.4 entries
Symptom · 01
EACCES error when running npm install -g
Fix
Run npm config get prefix. If it's /usr/lib/node_modules, reconfigure to ~/.npm-global and update your PATH. Or use nvm instead.
Symptom · 02
npm install succeeds but later require() fails with MODULE_NOT_FOUND
Fix
Check that the package appears in node_modules or in package.json under dependencies. Verify your require path is correct. If using multiple packages, check for hoisting issues in npm v6 vs v7+.
Symptom · 03
Version conflicts after npm install — "conflicting peer dependency"
Fix
Use npm ls <package> to see the tree. Common fix: add a --legacy-peer-deps flag temporarily, or better, manually align the conflicting versions. npm v7+ enforces peer deps strictly.
Symptom · 04
Audit warnings about moderate or high severity vulnerabilities
Fix
Run npm audit fix to apply safe updates automatically. For breaking changes, manually update the offending packages. Use npm audit --json to get details.
★ npm Quick Debug CommandsWhen something breaks, these commands tell you exactly what's wrong.
Package not found / version mismatch
Immediate action
Check what's installed: `npm ls <package>`
Commands
npm list --depth=0
npm view <package> versions
Fix now
npm install <package>@<exact-version> or delete node_modules and reinstall
Global command (like `create-react-app`) not found+
Immediate action
Check npm global path: `npm bin -g`
Commands
npm list -g --depth=0
which <command> (or `where` on Windows)
Fix now
Ensure global prefix is in PATH or install locally via npx
Slow install / network timeouts+
Immediate action
Check registry: `npm config get registry`
Commands
npm cache verify
npm config set registry https://registry.npmmirror.com (or your mirror)
Fix now
Clear cache and retry: npm cache clean --force then npm install
Lockfile conflicts in Git (package-lock.json merge conflicts)+
Immediate action
Keep the lockfile from the base branch and rerun npm install
Commands
git checkout --theirs package-lock.json (or yours)
npm install (npm resolves conflicts automatically)
Fix now
Commit the regenerated lockfile — never manually edit package-lock.json
dependencies vs devDependencies at a Glance
AspectdependenciesdevDependencies
PurposeCode your app needs to actually runTools you need while building/testing
Examplesexpress, axios, mongoose, reactjest, eslint, typescript, webpack
Install commandnpm install <package>npm install <package> --save-dev
Installed in production?Yes — alwaysNo — skipped with npm install --production
Appears in package.json under"dependencies": {}"devDependencies": {}
Risk if missing in prodApp crashes or won't startNo impact — prod doesn't need them
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
check-npm-installed.shnode --versionWhat npm Actually Is and Why It Was Built
create-project.shmkdir my-first-node-projectCreating Your First package.json
index.jsconst axios = require('axios');Installing Packages
package.json{npm Scripts
package-lock.json (excerpt){Understanding node_modules and package-lock.json
terminalnpm install eslint --save-devnpx vs npm

Key takeaways

1
npm is a package manager
it lets you install, share, and manage reusable code. It installs automatically with Node.js, so you don't need to set it up separately.
2
package.json is your project's identity card and shopping list
it records the project name, version, scripts, and every package the project depends on.
3
dependencies are packages your app needs to run in production (like express or axios). devDependencies are tools you only need while building or testing (like jest or eslint). Getting this wrong wastes resources on production servers.
4
npm scripts in package.json are shortcut commands for your whole team. 'npm start' and 'npm test' are built-in shortcuts; every other custom script runs with 'npm run <script-name>'.
5
Always commit package-lock.json to Git. It locks exact versions of every dependency and prevents irreproducible builds.
6
Metadata fields matter
browser, private, engines, homepage, bugs, and files are not optional. They define your package's contract with users and tools. Use private: true for internal projects and files to slim down published packages.
7
npx vs npm
know the difference** — Use npm install for dependencies, npx for execution. npx runs local binaries and one-off tools without global installs. Avoid npx in production scripts; use npm scripts instead.
8
LockfileVersion is critical for reproducibility
Version 2 is the standard for npm 7+. Enforce it with .npmrc or CI flags to prevent non-deterministic installs across your team.
9
LockfileVersion Matters
Always use lockfileVersion 3 with npm 9+ for deterministic installs and workspace support. Upgrade your lockfile explicitly to avoid silent failures.
10
npx for One-Offs, npm run for Repetition
Use npx to run tools without installing globally, but prefer npm run scripts for project-specific commands to ensure version consistency and speed.
11
Pre/Post Hooks Are Built-In Automation
Leverage npm's implicit pre/post hooks for simple task chaining like linting before builds. For complex pipelines, use explicit && chaining to avoid confusion.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between dependencies and devDependencies in packa...
Q02JUNIOR
If a colleague clones your Node.js repository and the node_modules folde...
Q03SENIOR
What does the caret symbol (^) mean in a version like ^1.4.2 inside pack...
Q04SENIOR
Explain what happens when you run `npm install` and how the lockfile ens...
Q01 of 04JUNIOR

What is the difference between dependencies and devDependencies in package.json, and can you give a real-world example of each?

ANSWER
dependencies are packages your application needs to run in production — for example, express (web server) or axios (HTTP client). devDependencies are tools you only need during development — like jest (testing) or eslint (linting). When you deploy with npm install --production, devDependencies are skipped, making deploys faster and smaller. A concrete example: If you build an API with Express and write tests with Jest, express is a dependency and jest is a devDependency. If you accidentally put jest in dependencies, your production server will download all of Jest's files (including test runners you never use) for no reason.
FAQ · 10 QUESTIONS

Frequently Asked Questions

01
What is the difference between npm and Node.js?
02
What is package-lock.json and do I need it?
03
Can I edit package.json by hand or should I only use npm commands?
04
Why should I not use sudo with npm?
05
What's the difference between npm and npx?
06
What is the difference between `npm install` and `npx`?
07
How do I enforce a specific lockfileVersion across my team?
08
Can pre/post hooks be nested? What happens if a pre-hook fails?
09
How do I upgrade my package-lock.json to lockfileVersion 3?
10
Can I use pre/post hooks with custom scripts?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.

Follow
Verified
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
🔥

That's Node.js. Mark it forged?

9 min read · try the examples if you haven't

Previous
Node.js Event Emitter
10 / 47 · Node.js
Next
Node.js File System Module