NPM Basics: What It Is and Why It Matters
npm (Node Package Manager) is the default package manager for Node.js and the largest software registry in the world. As of early 2026, the npm registry hosts over 2.5 million packages. When you run npm install express, you are pulling code from that registry into your project. When you run npm publish, you are pushing code into it.
Every modern JavaScript and TypeScript project depends on npm or one of its alternatives. Whether you are building a React frontend, a Node.js API, a CLI tool, or a full-stack application, your dependency tree is managed through a package manager. Getting this right is not a nice-to-have -- it directly affects your build reproducibility, deployment reliability, and application security.
npm does three things:
- Installs packages from the registry into your project's
node_modulesdirectory - Manages dependency versions using semantic versioning ranges and lock files
- Runs scripts defined in your
package.json-- build commands, test suites, linting, deployment, and custom automation
npm ships with every Node.js installation. No separate download required. Check your version with npm --version and update to the latest with npm install -g npm@latest.
Your dependency tree is your supply chain. Every package you install brings its own dependencies, which bring their own dependencies. A typical React application pulls in 800 to 1,500 packages. Understanding how npm resolves, installs, and locks these dependencies is not optional knowledge -- it is operational security.
The Anatomy of package.json
package.json is the manifest file for every Node.js project. It declares your project's identity, its dependencies, and the scripts that build, test, and run it. Here is a complete example with every field that matters.
{
"name": "my-api-server",
"version": "2.4.1",
"description": "REST API for the inventory management system",
"main": "dist/index.js",
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"test": "vitest run",
"test:watch": "vitest",
"lint": "eslint src/",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"express": "^4.21.0",
"zod": "^3.23.0",
"drizzle-orm": "^0.35.0"
},
"devDependencies": {
"typescript": "^5.6.0",
"vitest": "^2.1.0",
"eslint": "^9.12.0",
"@types/express": "^5.0.0",
"tsx": "^4.19.0"
},
"engines": {
"node": ">=20.0.0"
},
"license": "MIT"
}
Key Fields Explained
- name -- Must be lowercase, URL-safe. If you publish to npm, this is the package name users will
npm install. - version -- Follows semantic versioning (see next section). Required for published packages.
- type -- Set to
"module"to use ES modules (import/export) instead of CommonJS (require). In 2026, most new projects use ES modules. - scripts -- Named commands you run with
npm run <name>. Thestartandtestscripts are special -- you can run them without therunkeyword:npm start,npm test. - dependencies -- Packages required at runtime. These ship with your application.
- devDependencies -- Packages needed only during development -- compilers, test frameworks, linters. Not installed in production when you run
npm install --productionornpm ci --omit=dev. - engines -- Declares which Node.js versions your project supports. npm warns (but does not block) when the version does not match unless you set
engine-strict=truein.npmrc.
Need to scaffold a package.json quickly? The NexTool Package.json Generator lets you fill in fields through a visual form and exports a correctly formatted file -- no manual JSON editing required.
Semantic Versioning: The Rules That Keep Things From Breaking
Every npm package version follows the format MAJOR.MINOR.PATCH -- for example, 4.21.3. The three numbers communicate what changed.
- MAJOR (4.x.x) -- Breaking changes. The API changed in a way that could break existing code. You must read the migration guide.
- MINOR (x.21.x) -- New features that are backward-compatible. Existing code continues to work.
- PATCH (x.x.3) -- Bug fixes that are backward-compatible. No new features, just corrections.
When you specify a dependency in package.json, you use a version range that tells npm which updates to accept automatically.
# Exact version -- installs only 4.21.3
"express": "4.21.3"
# Caret (^) -- allows minor and patch updates
# ^4.21.3 matches >=4.21.3 and <5.0.0
"express": "^4.21.3"
# Tilde (~) -- allows only patch updates
# ~4.21.3 matches >=4.21.3 and <4.22.0
"express": "~4.21.3"
# Greater than or equal
"express": ">=4.21.0"
# Range
"express": ">=4.18.0 <5.0.0"
# Wildcard -- any version (dangerous)
"express": "*"
The caret (^) is the default when you run npm install express. It is a reasonable default because most well-maintained packages follow semver: minor and patch updates should not break your code. But "should not" is not "will not." This is why lock files exist.
For application projects, use the caret (^) default and rely on your lock file for reproducibility. For critical infrastructure, consider pinning exact versions and updating manually after testing. The tradeoff is between automatic security patches and absolute control.
Lock Files: Why package-lock.json Exists
package.json declares version ranges. package-lock.json records the exact versions that were actually installed, including every transitive dependency in the entire tree.
Without a lock file, running npm install on two different machines at two different times can produce different node_modules directories. A new patch release of a dependency between your local install and the CI server install means you are testing locally against one set of code and deploying another. Lock files eliminate this class of bugs entirely.
# What package.json says:
"express": "^4.21.0"
# What package-lock.json records:
"express": {
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.3.tgz",
"integrity": "sha512-abc123..."
}
The integrity field is a hash of the package tarball. npm verifies this hash on every install, ensuring you get the exact same code. If someone tampers with a package on the registry, the integrity check fails and the install aborts.
npm install vs npm ci
This is one of the most important distinctions in npm and one that many developers get wrong.
| Behavior | npm install | npm ci |
|---|---|---|
| Reads | package.json | package-lock.json |
| Modifies lock file | Yes (updates it) | Never |
| Deletes node_modules first | No | Yes |
| Resolves version ranges | Yes | No (uses exact lock) |
| Speed | Slower | Faster |
| Deterministic | Not guaranteed | Yes |
| Best for | Development | CI/CD, production |
Use npm install during development when you are adding, removing, or updating packages. Use npm ci everywhere else -- CI pipelines, Docker builds, production servers. This single change catches an entire category of deployment bugs.
Essential npm Commands
These are the commands you will use daily. Each one deserves understanding beyond the basic syntax.
Installing Packages
# Install all dependencies from package.json
npm install
# Install a specific package (adds to dependencies)
npm install express
# Install as a dev dependency
npm install --save-dev vitest
# Install a specific version
npm install express@4.21.3
# Install globally (CLI tools)
npm install -g tsx
# Clean install from lock file (CI/production)
npm ci
# Install without optional dependencies
npm install --omit=optional
# Install production dependencies only
npm install --omit=dev
Updating Packages
# Check which packages are outdated
npm outdated
# Update packages within semver ranges
npm update
# Update a specific package
npm update express
# Jump to a new major version (interactive)
npx npm-check-updates -u
npm install
Removing Packages
# Remove a package
npm uninstall lodash
# Remove and clean from devDependencies
npm uninstall --save-dev jest
# Remove all node_modules and reinstall
rm -rf node_modules package-lock.json
npm install
Inspecting Your Dependency Tree
# List all installed packages (top-level)
npm list --depth=0
# List all installed packages (full tree)
npm list
# Find why a specific package is installed
npm explain package-name
# View info about a registry package
npm info express
# Check for duplicate packages
npm dedupe
For a visual breakdown of any package's size, dependencies, and metadata, use the NexTool NPM Package Analyzer. Paste a package name and see its bundle size, dependency count, weekly downloads, and version history at a glance.
Analyze Any npm Package Instantly
Check bundle size, dependencies, versions, and download stats -- directly in your browser.
Open NPM Package Analyzernpm vs yarn vs pnpm: Which Package Manager in 2026
npm is no longer the only option. Yarn and pnpm both solve real problems that npm historically had -- speed, disk efficiency, and determinism. Here is how they compare in 2026.
| Feature | npm | Yarn (Berry) | pnpm |
|---|---|---|---|
| Included with Node.js | Yes | Via corepack | Via corepack |
| Install speed | Good | Fast | Fastest |
| Disk usage | High (copies per project) | Low (PnP) / High (node_modules) | Lowest (content-addressable store) |
| Lock file | package-lock.json | yarn.lock | pnpm-lock.yaml |
| Monorepo workspaces | Basic | Advanced | Advanced |
| Plug'n'Play (no node_modules) | No | Yes (default in Berry) | No |
| Strict dependency isolation | No (hoisting) | Yes (PnP) | Yes (symlinks) |
| Ecosystem compatibility | 100% | ~95% (PnP breaks some tools) | ~98% |
npm: The Default
npm is the safe choice. It ships with Node.js, every tutorial uses it, and every CI system supports it out of the box. Recent versions (npm 10+) have closed much of the performance gap with yarn and pnpm. If you have no specific pain point, npm works fine.
Yarn (Berry / v4)
Yarn's killer feature is Plug'n'Play (PnP), which eliminates the node_modules directory entirely. Dependencies are stored as compressed archives and resolved through a .pnp.cjs file. This makes installs near-instant after the first time (zero-install when committed to Git) and eliminates the massive node_modules folder. The tradeoff is compatibility -- some tools still expect node_modules to exist and need patching or workarounds.
pnpm: The Performance Leader
pnpm stores every package version once in a global content-addressable store (~/.local/share/pnpm/store), then creates hard links from each project's node_modules into the store. This means installing the same version of React in 10 projects uses disk space only once. pnpm is also the strictest about dependency isolation -- your code can only import packages that are explicitly listed in package.json, catching phantom dependency bugs that npm and yarn miss.
# Enable corepack (ships with Node.js 16.13+)
corepack enable
# Use pnpm in a project
corepack use pnpm@latest
# pnpm commands mirror npm closely
pnpm install # Same as npm install
pnpm add express # Same as npm install express
pnpm remove lodash # Same as npm uninstall lodash
pnpm run build # Same as npm run build
pnpm dlx create-next-app # Same as npx create-next-app
For new projects in 2026, pnpm is the strongest choice -- fastest installs, lowest disk usage, strictest dependency resolution. For teams that want zero configuration overhead, npm is the pragmatic default. Choose yarn if you specifically want PnP zero-installs or use advanced workspace features. Pick one per organization and standardize.
Security Audits: Finding and Fixing Vulnerabilities
Your node_modules directory is a supply chain. Every one of those packages was written by a third party, and any one of them can contain vulnerabilities -- or be compromised in a supply chain attack. npm provides built-in tools to detect known vulnerabilities.
Running an Audit
# Full vulnerability report
npm audit
# JSON output for CI parsing
npm audit --json
# Only production dependencies
npm audit --omit=dev
# Summary output
npm audit --audit-level=moderate
The audit report shows each vulnerability's severity (critical, high, moderate, low), the affected package, the vulnerable version range, and the fixed version if one exists.
Fixing Vulnerabilities
# Auto-fix compatible updates (safe)
npm audit fix
# Force fixes including breaking changes (review carefully)
npm audit fix --force
# See what fix would do without applying
npm audit fix --dry-run
# Override a transitive dependency version
# Add to package.json:
{
"overrides": {
"vulnerable-package": ">=2.1.4"
}
}
Beyond npm audit
npm audit only checks the npm advisory database. For deeper analysis, consider these layers.
- Socket.dev -- Detects supply chain attacks: typosquatting, install scripts, obfuscated code, network calls. Add it to your GitHub pull requests.
- Snyk -- Broader vulnerability database with auto-fix PRs. Free for open-source projects.
- npm provenance -- Verify that a package was built from its claimed source repository using SLSA provenance attestations. Supported since npm 9.5.
- Lockfile-lint -- Verifies that your lock file only resolves packages from trusted registries and uses HTTPS.
When reviewing audit output, use the NexTool JSON Formatter to make the npm audit --json output readable. The tree view makes it easy to trace which top-level dependency pulls in a vulnerable transitive package.
Run npm audit in every CI pipeline. Set --audit-level=high to fail the build on high or critical vulnerabilities. Fixing vulnerabilities is not a quarterly task -- it is a continuous process that belongs in your deployment gate.
npm Best Practices for Production Projects
These practices separate hobby projects from production-grade dependency management.
1. Always Commit Your Lock File
For application projects, package-lock.json (or pnpm-lock.yaml or yarn.lock) must be in version control. Without it, you cannot guarantee reproducible builds. Add node_modules/ to .gitignore but never the lock file.
2. Use npm ci in CI/CD
Never run npm install in a CI pipeline. Use npm ci -- it is faster, deterministic, and fails loudly if the lock file is out of sync with package.json. This catches the common mistake of committing package.json changes without updating the lock file.
3. Separate dependencies from devDependencies
Test frameworks, type definitions, linters, and build tools belong in devDependencies. This matters when you deploy -- npm ci --omit=dev installs only what your application needs at runtime, producing smaller Docker images and faster cold starts.
4. Pin Node.js Versions
Use the engines field in package.json and a .nvmrc or .node-version file to declare which Node.js version your project uses. This prevents "works on my machine" issues when team members run different Node.js versions.
# .nvmrc
20.11.0
# .npmrc (enforce engine check)
engine-strict=true
5. Audit Regularly and Automate It
Set up Dependabot, Renovate, or Snyk to open PRs when dependency updates are available. Review and merge them weekly. Do not let your dependency updates accumulate into a terrifying mega-update that nobody wants to touch.
6. Use .npmrc for Consistent Configuration
A project-level .npmrc file ensures everyone on the team uses the same npm settings.
# .npmrc
engine-strict=true
save-exact=true
fund=false
audit-level=high
save-exact=true pins exact versions by default instead of caret ranges. fund=false suppresses the funding message after installs. audit-level=high only shows high and critical vulnerabilities.
7. Clean Up Unused Dependencies
Over time, packages get installed for features that are later removed, but the dependency stays in package.json. Use npx depcheck to find unused dependencies and remove them. Every unnecessary package is a larger bundle, a slower install, and a wider attack surface.
Frequently Asked Questions
What is the difference between npm install and npm ci?
npm install reads package.json, resolves dependency versions based on semver ranges, and writes or updates package-lock.json. It is designed for development, where you may be adding or updating packages. npm ci (clean install) deletes the existing node_modules directory, then installs the exact versions specified in package-lock.json without modifying it. npm ci is faster, deterministic, and designed for CI/CD pipelines and production builds where reproducibility matters. Always use npm ci in automated environments.
Should I commit package-lock.json to version control?
Yes, always commit package-lock.json to version control for application projects. The lock file records the exact version of every installed dependency, including transitive dependencies. Without it, different developers and CI servers may install different versions of the same package, leading to "works on my machine" bugs. The only exception is library packages published to npm -- libraries should not commit lock files because consumers will use their own lock files. For applications, the lock file is essential for reproducible builds.
What do the tilde (~) and caret (^) mean in package.json versions?
The caret (^) allows updates that do not change the leftmost non-zero digit. For example, ^1.2.3 allows any version from 1.2.3 up to but not including 2.0.0. The tilde (~) allows only patch-level updates. For example, ~1.2.3 allows any version from 1.2.3 up to but not including 1.3.0. The caret is the default when you run npm install, and it strikes a reasonable balance between getting bug fixes and avoiding breaking changes. Use exact versions (no prefix) or tilde ranges in production applications where stability is critical.
How do I fix npm security vulnerabilities?
Run npm audit to see a report of known vulnerabilities in your dependency tree. Run npm audit fix to automatically install compatible patched versions where available. For breaking-change fixes that require major version bumps, run npm audit fix --force, but review the changes carefully because this can break your application. If a vulnerability is in a deeply nested transitive dependency, you can use the overrides field in package.json to force a specific version. Always run your test suite after fixing vulnerabilities to ensure nothing breaks.
Which package manager should I use: npm, yarn, or pnpm?
npm is the default and works without additional installation. It has closed the performance gap significantly in recent versions and is the safest choice for most projects. Yarn offers Plug'n'Play mode for zero-install workflows and has strong monorepo support via workspaces. pnpm is the fastest and most disk-efficient option, using a content-addressable store and hard links to avoid duplicating packages across projects. For new projects in 2026, pnpm is the best choice if performance and disk usage matter to you. For teams that want zero setup friction, npm is the pragmatic default. Yarn is best if you use its PnP mode or need its advanced workspace features.
Explore 150+ Free Developer Tools
NPM Package Analyzer is just the start. NexTool has free tools for JSON formatting, package.json generation, regex testing, encoding, hashing, and much more.
Browse All Free Tools