npm and package.json — EACCES Permission Error Without sudo
The EACCES error when running npm install -g is caused by root ownership from sudo.
20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.
- ✓Basic programming fundamentals
- ✓A computer with internet access
- ✓Willingness to follow along with examples
- 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
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.
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.
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.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
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.
# 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.
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.
# 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
Installing Packages — Dependencies vs devDependencies
Now for the fun part: pulling in other people's code. When you run 'npm install <package-name>', 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.
// 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);
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.
{
"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"
}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.
{
"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-..."
}
}
}npm install to regenerate correctly.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.
{
"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
}
}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.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.
{
"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
}
}
}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.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).
{
"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"
}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.exports field is your package's firewall. Use it to enforce public API boundaries and kill hidden dependencies.EACCES Permission Error During npm install -g
/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.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.- 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.
npm config get prefix. If it's /usr/lib/node_modules, reconfigure to ~/.npm-global and update your PATH. Or use nvm instead.require() fails with MODULE_NOT_FOUNDnode_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+.npm install — "conflicting peer dependency"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.npm audit fix to apply safe updates automatically. For breaking changes, manually update the offending packages. Use npm audit --json to get details.npm list --depth=0npm view <package> versionsnpm list -g --depth=0which <command> (or `where` on Windows)npm cache verifynpm config set registry https://registry.npmmirror.com (or your mirror)npm cache clean --force then npm installgit checkout --theirs package-lock.json (or yours)npm install (npm resolves conflicts automatically)| Aspect | dependencies | devDependencies |
|---|---|---|
| Purpose | Code your app needs to actually run | Tools you need while building/testing |
| Examples | express, axios, mongoose, react | jest, eslint, typescript, webpack |
| Install command | npm install <package> | npm install <package> --save-dev |
| Installed in production? | Yes — always | No — skipped with npm install --production |
| Appears in package.json under | "dependencies": {} | "devDependencies": {} |
| Risk if missing in prod | App crashes or won't start | No impact — prod doesn't need them |
| File | Command / Code | Purpose |
|---|---|---|
| check-npm-installed.sh | node --version | What npm Actually Is and Why It Was Built |
| create-project.sh | mkdir my-first-node-project | Creating Your First package.json |
| index.js | const axios = require('axios'); | Installing Packages |
| package.json | { | npm Scripts |
| package-lock.json (excerpt) | { | Understanding node_modules and package-lock.json |
Key takeaways
Common mistakes to avoid
4 patternsCommitting node_modules to Git
node_modules/ to .gitignore before your first commit. If already committed, run git rm -r --cached node_modules then commit again.Installing everything as a regular dependency instead of using --save-dev
dependencies instead of devDependencies. Production servers download thousands of files they'll never use, increasing deploy time and attack surface.--save-dev. Run npm ls --prod to verify only runtime packages are present.Deleting package-lock.json because it 'looks confusing'
npm install a week apart can get different sub-dependency versions, causing subtle 'works on my machine' bugs that are hard to reproduce.npm install to regenerate correctly.Using `npm init` with interactive questions instead of `npm init -y`
npm init -y for defaults, then manually edit package.json. It's faster and produces a known-good starting point.Interview Questions on This Topic
What is the difference between dependencies and devDependencies in package.json, and can you give a real-world example of each?
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.If a colleague clones your Node.js repository and the node_modules folder isn't there, what command do they run and how does npm know what to install?
npm install. npm reads the package.json file to see which packages are listed under dependencies and devDependencies, then downloads the appropriate versions from the npm registry. If a package-lock.json exists, npm uses the exact versions specified there to guarantee identical installations across machines.
The install process: 1) npm resolves version ranges against the registry, 2) checks the lockfile for pinned versions, 3) downloads tarballs, 4) extracts them into node_modules, 5) runs any install scripts if present.What does the caret symbol (^) mean in a version like ^1.4.2 inside package.json, and how is that different from specifying an exact version like 1.4.2 with no symbol?
^1.4.2 means 'install any version from 1.4.2 up to, but not including, 2.0.0'. It allows minor and patch updates but not major breaking changes. An exact version 1.4.2 without any symbol means 'install only 1.4.2, never update'.
The tilde (~1.4.2) is stricter: it allows only patch updates (1.4.3, 1.4.4, etc.) but not minor version bumps.
In production, you typically want caret ranges to get bug fixes automatically, but serious projects pin exact versions via lockfile. The lockfile always records the exact installed version, so the range in package.json is just a declaration of intent.Explain what happens when you run `npm install` and how the lockfile ensures deterministic builds.
npm install, npm reads package.json to get the list of dependencies and their version ranges. It then consults the package-lock.json (if present) to see if an exact version tree has been recorded. If the lockfile exists and matches the package.json ranges, npm installs exactly those versions, skipping resolution. If no lockfile exists, npm resolves the latest versions matching the ranges, builds a dependency tree, and creates a new lockfile.
The lockfile is deterministic because it records the exact version, integrity hash, and dependency tree of every package — including sub-dependencies. This ensures that every person, CI server, and production instance gets the exact same node_modules, preventing "works on my machine" bugs. Without it, version ranges like ^1.4.2 could resolve to different minor versions over time.Frequently Asked Questions
Node.js is a runtime — it's the engine that lets you run JavaScript outside of a browser. npm is a package manager that comes bundled with Node.js. Think of Node.js as a car engine and npm as the mechanic's tool kit — one runs your code, the other helps you manage the parts you need to build it.
package-lock.json is an automatically generated file that records the exact version of every package and sub-package installed in your project. You should always commit it to Git. It ensures that every developer on your team and every production server installs the exact same versions, preventing subtle 'works on my machine' bugs.
You can absolutely edit package.json by hand — it's just a plain text JSON file. Developers do it all the time to update scripts, add metadata, or tweak version ranges. After editing it manually, run 'npm install' to make sure your node_modules folder reflects the changes you made.
Using sudo changes ownership of installed files to root, which causes permission errors for other users and can break global commands like create-react-app. Instead, configure npm to use a user-level directory by running npm config set prefix ~/.npm-global and adding that to your PATH. Better yet, use nvm to manage Node versions without any permission issues.
npm installs packages globally or locally and adds them to your project. npx executes packages without installing them permanently — ideal for running one-off tools like create-react-app without cluttering global space. For example, npx create-react-app my-app downloads and runs the latest version without leaving a permanent install.
20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.
That's Node.js. Mark it forged?
6 min read · try the examples if you haven't