# What Is npm? The Package Manager Behind JavaScript Projects

> npm is the standard package manager for Node.js, providing a CLI and registry to manage over 2.1 million JavaScript packages with exact version locking.

Source: https://camnangai.com/en/engineering/what-is-npm  
Published: 2026-08-07

**npm** is the standard package manager for Node.js, providing the infrastructure required to
  share, version, and install code dependencies across the JavaScript ecosystem.

It operates as a three-tier system: a public registry of versioned software, a command-line
interface (CLI) for local package management, and a website for discovery and organization
management.

By establishing a centralized protocol for dependency distribution, npm lets you manage complex
[JavaScript](https://camnangai.com/en/engineering/what-is-javascript) codebases, ensuring that specific library versions
are retrieved and integrated into the application runtime. This infrastructure supports both
open-source collaboration and private organizational workflows through a permission-based registry.


  For any engineer working with the [Node.js](https://camnangai.com/en/engineering/what-is-nodejs) runtime, npm is the
  default mechanism for retrieving, updating, and auditing code.

Its components work together to keep dependency trees navigable and reproducible across development
and deployment environments.

![npm, the package manager behind JavaScript projects — article cover](https://camnangai.com/articles/npm-la-gi/01-infographic-en.webp)

## What is npm?

**npm** has three parts that work together: the website, the command-line interface (CLI), and the
public registry. The website is where you find packages and manage organizations and teams. The CLI
is what resolves dependencies on your machine and on CI/CD runners. Behind both sits the registry,
the world's largest software database, which hosted over 2.1 million packages as of late 2022.

![The three parts of npm: the website, the command-line interface, and the registry](https://camnangai.com/articles/npm-la-gi/02-en.webp)

You reach for npm to pull external packages into an application, run standalone tools via `npx`
without installing them permanently, and control whether a module is public or private. Larger
organizations use the same hooks to restrict code access to specific teams while keeping one
workflow for internal and external dependencies.

Underneath, the CLI talks to the registry's CouchDB-based database to manage package metadata. When you initiate a request, the CLI queries the registry for a JSON
document containing the package's version history and metadata. This metadata includes the specific
URLs for compressed tarballs and their associated cryptographic hashes.

The CLI does not merely download files; it performs a multi-step resolution process. It first
reconciles the requested version range against available versions in the registry, then fetches the
manifest to identify transitive dependencies. Once the full dependency tree is resolved, the CLI
downloads the required tarballs, verifies their integrity against the registry's hash, and extracts
them into the local environment.

## package.json: the file that declares your project

The `package.json` file is the manifest for a Node.js project, acting as the source of truth for
dependency resolution. It tracks metadata such as the project name and version, while explicitly
declaring the libraries required to execute the application. Any developer or automated environment
can replicate the project's dependency tree by referencing the versions documented in the manifest.
Without this file, the **npm** CLI cannot identify which libraries to download or how to execute
project-specific scripts.

![Dependency types in package.json: dependencies, devDependencies, optionalDependencies and peerDependencies](https://camnangai.com/articles/npm-la-gi/03-en.webp)

The manifest splits dependencies in two, and the split matters. Libraries the application needs at
runtime go under `dependencies`.
Conversely, `devDependencies` are reserved for development-time tools, such as the Jest testing
framework or build utilities like Webpack, which are not needed in production. When deploying, you
can use `--omit=dev` to exclude these development tools, reducing the final footprint and minimizing
the attack surface of the production build.

Since npm 5, the CLI updates this manifest for you. Flags like `--save-dev` (shorthand `-D`) or `--save-optional` (shorthand `-O`) during
installation automatically create the corresponding entry in the `package.json` file. Optional
dependencies are unique because their failure to build or install does not cause the overall
installation to fail; your application logic must account for the potential absence of these
modules. `peerDependencies` serve a different purpose, common when building a plugin or companion
library that requires the host project to already provide a specific version of a base library such
as [React](https://camnangai.com/en/engineering/what-is-react).

The manifest also carries the Semantic Versioning ranges that determine how the CLI updates
packages. By defining these ranges, you control the balance between inheriting the latest security
patches and maintaining a stable, unchanging codebase. A typical project uses this manifest to
manage dozens or hundreds of direct dependencies, which in turn pull in thousands of transitive
dependencies, making the manifest the anchor for the entire project architecture.

```json
{
  "name": "enterprise-service-mesh",
  "version": "1.0.0",
  "dependencies": {
    "express": "^4.18.2",
    "helmet": "^7.0.0"
  },
  "devDependencies": {
    "jest": "^29.5.0",
    "typescript": "^5.0.0"
  },
  "optionalDependencies": {
    "fsevents": "^2.3.2"
  }
}
```

## Semver: what ^ and ~ actually allow

Semantic Versioning (Semver) is the protocol used to signal API stability and breaking changes via
a `MAJOR.MINOR.PATCH` format. MAJOR versions indicate breaking API changes, MINOR versions add
backward-compatible functionality, and PATCH versions provide backward-compatible bug fixes. **npm**
relies on these signals to decide whether to fetch a newer version from the registry or stay with
the current local installation.

![The caret allows minor and patch updates while the tilde allows only patch updates](https://camnangai.com/articles/npm-la-gi/04-en.webp)

The tilde (`~`) is a conservative specifier that allows only patch updates: `~1.2.3` allows anything
below `1.3.0`. It assumes the maintainer will only include non-breaking bug fixes in patch releases,
which gives your project stability while still inheriting security patches automatically. The caret
(`^`) is the default specifier, allowing minor and patch updates: `^1.2.3` allows anything below
`2.0.0`. It became the default save prefix in npm 1.4.22 because it lets projects safely inherit
un-backported bug fixes.

The "Magic Zero" (`0.x.y`) behavior is a critical nuance. Semver treats major version zero as
initial development, where the API is considered unstable, so the caret becomes more cautious. For
`0.0.z` releases, the caret allows zero flexibility and pins to the exact version. For `0.y.z`
releases, the caret behaves exactly like a tilde, allowing only patch updates. This logic exists
because, in the pre-1.0.0 ecosystem, the community treats the minor version as the indicator for
breaking changes.

This "1.0.0 anxiety" is prevalent throughout the registry: approximately 82% of all packages remain
at major version zero. Authors often stay in this state to avoid the perceived commitment of a
stable API, essentially reinterpreting the Semver spec to use the second segment as the primary
breaking-change indicator. To counter the trend, modern versions of `npm init` (starting from
v1.4.22) changed the default starting version from `0.0.0` to `1.0.0`, pushing developers through
the imaginary 1.0.0 barrier toward more rigorous major version bumps.

## What npm install does to node_modules

When you execute `npm install`, the CLI initiates a resolution process to reconcile the dependency
requirements defined in your project manifest. The tool parses the `package.json` file, fetches the
necessary tarballs from the registry, and populates a local `node_modules` folder. This folder
stores all libraries and their nested dependencies, making them available to the Node.js runtime.
The process verifies that every dependency's internal requirements are also met, often leading to a
large expansion of the local file count.

![npm v2 nested dependencies deeply while npm v3 hoists them into a flat structure](https://camnangai.com/articles/npm-la-gi/05-en.webp)

The shape of `node_modules` changed to save space and dodge a system limit. In npm v2, the
dependency tree was strictly nested, producing deep folder
structures that often exceeded path length limits on Windows. Since npm v3, the tool uses a flat
directory structure, hoisting as many dependencies as possible to the top level of `node_modules`.
This flattening reduces duplication and lets different packages requiring the same version share a
single physical installation on disk.

Version conflicts are where the installation algorithm earns its keep. If two dependencies
require different, incompatible versions of the same sub-dependency, **npm** cannot hoist both to
the top level. It nests the conflicting version inside the specific package that requires it, while
keeping the primary version at the root. This dual-model resolution ensures the Node.js `require`
mechanism finds the correct version of every library based on the filesystem hierarchy.

Since npm 5, the CLI also updates the manifest during installation. If you install a package using
`npm install <package-name>`, the tool automatically adds the entry to your `package.json`, removing
the historical requirement for the `--save` flag. Running the install command without arguments
syncs the `node_modules` folder with the current `package.json` and `package-lock.json` definitions.

## package-lock.json and npm ci: why installs stay identical

The `package-lock.json` file is an automatically generated snapshot of the entire dependency tree as
it existed during the last installation. While `package.json` allows flexible version ranges using
caret or tilde, the lockfile records the exact version of every single package installed, including
every nested transitive dependency. Every engineer on a team and every build server installs an
identical set of files, which eliminates the "it works on my machine" failures caused by one
environment pulling a slightly newer minor version.

![npm install may update the lockfile while npm ci wipes node_modules and installs exactly from it](https://camnangai.com/articles/npm-la-gi/06-en.webp)

The lockfile also verifies what you download, through an `integrity` field on every entry. This field contains a Subresource Integrity string, typically using a SHA-512
hash, to verify that the downloaded tarball has not been corrupted or tampered with. When an install
is triggered, **npm** compares the hash of the downloaded file against the hash stored in the
lockfile. On a mismatch, the CLI aborts, protecting the environment from code that differs from the
originally audited version.

The `npm ci` command is designed for automated environments where deterministic builds are
mandatory. Unlike `npm install`, which is permitted to modify the lockfile if it finds newer
compatible versions, `npm ci` strictly adheres to `package-lock.json`. It throws an error and fails
the build if the lockfile does not match the manifest. It also deletes the existing `node_modules`
folder before starting, so no stale artifacts remain — making it both faster and more reliable for
production deployments.

Always commit `package-lock.json` to version control so the exact state of your dependencies is
preserved. The
lockfile also speeds up installation by providing a pre-computed dependency tree, reducing the I/O
and CPU overhead of resolving complex version constraints. Using `npm ci` in your deployment
pipeline ensures the code running in production is an exact match of the code that passed your
tests.

## npm scripts: running project tasks with one command

The `scripts` property in `package.json` lets you define aliases for complex or frequently used
command-line tasks. This provides a standardized way to execute project-specific workflows, such as
starting a local development server, running a test suite, or executing a production build. Scripts
hide the complexity of underlying tool configurations — long strings of arguments for Webpack, Vite,
or TypeScript — behind simple, memorable commands.

Instead of requiring every developer to remember long strings of CLI arguments, you provide a
unified interface via `npm run <task-name>`. A developer only needs to type `npm run build` to
execute a process that might involve multiple environment variables and configuration flags.
Standard scripts like `start` and `test` are common enough that they can be run without the `run`
keyword, though `npm run` remains the universal invocation for custom aliases.

These tasks execute within a shell environment that has the local `node_modules/.bin` folder added
to PATH. This is a critical detail because it lets you call installed binaries directly without
their full file paths. If you have Jest installed as a devDependency, your test script can simply be
`jest` rather than `./node_modules/.bin/jest`. That isolation ensures the project uses the specific
version of the tool installed in its own `node_modules`, rather than a different version installed
globally.

Common script configurations include tools for testing, linting, and server execution, and you can
chain scripts together using standard shell operators. These tasks are the primary way CI/CD
pipelines interact with your project, providing a predictable entry point for building and testing.
Centralizing these commands in the manifest means the logic for how the application is built and run
is versioned alongside the source code.

```json
{
  "scripts": {
    "start": "node server.js",
    "test": "jest",
    "build": "webpack --config webpack.config.js"
  }
}
```

## Install scripts and supply chain risk: what npm v12 changed

The **npm** ecosystem faces substantial supply chain risk from lifecycle hooks such as `preinstall`
and `postinstall`, which let package authors execute arbitrary shell code on your machine during
installation. Threat actors have weaponized these scripts in campaigns such as the Shai-Hulud worm
to steal sensitive credentials, specifically `NPM_TOKEN` and `GITHUB_TOKEN` values. Those tokens are
then used to infect and republish legitimate packages automatically, creating a wormable propagation
vector that compromises the registry at scale.

![npm v12 turns three install behaviours into opt-in settings](https://camnangai.com/articles/npm-la-gi/08-en.webp)

Attacks like the Miasma RAT have demonstrated increasing technical depth. These payloads target
continuous integration runners, reading `/proc/<Runner.Worker>/mem` to extract OpenID Connect tokens
minted lazily in runner memory, then posting them directly to the registry. Others poison
[GitHub](https://camnangai.com/en/engineering/what-is-github) Actions caches, overwriting stored pnpm or npm artifacts so
malicious code enters a maintainer's later builds.

Adversaries now use resilient, decentralized command-and-control infrastructure. Recent variants
deliver secondary payloads over IPFS and monitor Ethereum smart contracts as a decentralized
registry of active nodes, with Nostr relays and BitTorrent DHT routines as fallbacks when primary
addresses are blocked. One payload installs a background service that polls the GitHub API with the
stolen token every 60 seconds; if the token is revoked, it executes `rm -rf ~/` and destroys the
user's home directory.

npm v12 answers this by changing the defaults themselves. Three install behaviors that used to run
automatically are now opt-in: `allowScripts` defaults to off, so dependency lifecycle scripts
(`preinstall`, `install`, `postinstall`) and implicit `node-gyp` builds no longer run; `--allow-git`
defaults to `none`, so Git dependencies are no longer resolved; and `--allow-remote` defaults to
`none`, so dependencies from remote URLs are no longer resolved. To review and approve the scripts
you trust, run `npm approve-scripts --allow-scripts-pending` and commit the resulting allowlist in
`package.json`. All three were available behind warnings in npm 11.16.0+, so you can prepare before
upgrading.

## npm, Yarn or pnpm: how they differ

While **npm** is the industry standard, [Yarn](https://camnangai.com/en/engineering/what-is-yarn) and pnpm take different
architectural approaches. npm
historically used a file-copying model to build `node_modules`, which consumes significant disk
space when multiple projects share dependencies. pnpm instead uses content-addressable storage, with
all packages kept in a single global store on disk and linked into project-specific `node_modules`
via hard links and symlinks. That reduces I/O overhead and prevents phantom dependencies, where code
can access packages not explicitly listed in its manifest.

![Three dependency layouts: the hoisted flat tree of npm and Yarn Classic, Yarn PnP, and pnpm's content-addressable store](https://camnangai.com/articles/npm-la-gi/09-en.webp)

Yarn was originally developed to address performance and consistency issues in older versions of
npm, introducing its own lockfile format. Each manager has distinct performance characteristics;
pnpm is generally faster at installation thanks to its linking strategy and avoidance of redundant
network requests. Yarn pioneered many of the caching and parallelization techniques that were
eventually adopted into the npm CLI itself.

The internal structure of `node_modules` varies between these tools, which affects how the Node.js
runtime resolves modules. npm and Yarn typically create a flatter folder structure to manage deep
dependency trees, whereas pnpm's symlink-heavy approach creates a nested structure that more
accurately reflects the real dependency graph. That forces you to be explicit about the libraries
you use, since sub-dependencies are not hoisted to the root by default. Each tool uses its own
lockfile — `package-lock.json`, `yarn.lock`, and `pnpm-lock.yaml` respectively — and all three talk
to the same public registry using the same `package.json` format.

Choosing between them is a trade-off between standard compatibility and performance. npm is the most
compatible and ships with Node.js, which makes it the safest default. pnpm is preferred for
monorepos or machines with limited disk space. Whichever you pick, never mix two managers in one
project: their lockfiles conflict and the resulting environment stops being reproducible.

## Where to start with npm

Start a new project with `npm init` to generate the manifest, then lean on the two commands that
keep an environment predictable: `npm ci` for every automated pipeline, and `npm audit` on a regular
cadence to surface known vulnerabilities in your dependency tree. Commit `package-lock.json` to
version control so the whole team resolves to the same tree.

npm remains the right default for most projects, provided you understand three things: how version
ranges are locked, how integrity hashes protect what you download, and what `npm install` is allowed
to execute on your machine. If you run dozens of projects on one machine, evaluating pnpm for its
disk efficiency is worth the afternoon — see
[DevOps](https://camnangai.com/en/engineering/what-is-devops) practice for where these commands sit in a delivery
pipeline.

## References

- [About npm — npm Docs](https://docs.npmjs.com/about-npm)
- [An introduction to the npm package manager — Node.js Learn](https://nodejs.org/learn/getting-started/an-introduction-to-the-npm-package-manager)
- [What is npm? A Node Package Manager Tutorial for Beginners — freeCodeCamp](https://www.freecodecamp.org/news/what-is-npm-a-node-package-manager-tutorial-for-beginners/)
- [package.json — npm Docs](https://docs.npmjs.com/cli/v11/configuring-npm/package-json)
- [Semver: Tilde and Caret — NodeSource](https://nodesource.com/blog/semver-tilde-and-caret)
- [How to Understand package-lock.json in Node.js — OneUptime](https://oneuptime.com/blog/post/2026-01-22-nodejs-package-lock-json/view)
- [npm vs Yarn vs pnpm: Package Manager Selection Guide 2026](https://tomodahinata.com/en/blog/npm-vs-yarn-vs-pnpm-package-manager-comparison-guide)
- [npm install-time security and GAT bypass2fa deprecation — GitHub Changelog](https://github.blog/changelog/2026-07-08-npm-install-time-security-and-gat-bypass2fa-deprecation/)
- [The npm Threat Landscape: Attack Surface and Mitigations — Unit 42](https://unit42.paloaltonetworks.com/monitoring-npm-supply-chain-attacks/)
