What is npm?
npm is the default package manager for JavaScript and the largest software registry in the world. It ships with Node.js, installs the dependencies your project declares, resolves their versions and gives you a consistent task runner through scripts.
Almost every JavaScript project depends on npm, even if it uses a different client underneath. Understanding what it actually does — how versions resolve, what the lockfile is for and why CI uses a different command — removes a whole category of confusing errors.
package.json
The manifest describes your project. It holds metadata, dependencies and scripts.
{
"name": "my-app",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "vitest run",
"lint": "eslint ."
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"vite": "^6.0.0",
"vitest": "^3.0.0"
}
}
dependencies are needed at runtime; devDependencies are tools used to develop and build. The private flag prevents accidental publishing, and type: "module" opts into ES modules.
Version ranges
npm uses semantic versioning: major.minor.patch. A range in package.json describes which updates you accept.
| Range | Meaning | Allows |
|---|---|---|
1.2.3 |
exact | nothing |
~1.2.3 |
patch updates | 1.2.4, 1.2.5 |
^1.2.3 |
minor and patch | 1.3.0, 1.4.2 |
* |
anything | any version |
The caret is the default and the sensible choice for most dependencies. Exact versions block bug fixes and security patches, while the lockfile already guarantees reproducibility.
The lockfile
package-lock.json records the exact version and integrity hash of every package in the tree, including transitive dependencies.
- Commit it to version control.
- Never edit it by hand.
- Regenerate it deliberately when you intend to update.
- Let it be the single source of truth for installed versions.
Without a lockfile, two installs of the same package.json can produce different trees, and a bug that only appears in production becomes nearly impossible to reproduce.
Installing dependencies
The two main install commands have different purposes.
# development: resolve ranges, may update the lockfile
npm install
# CI and clean environments: exact install from the lockfile
npm ci
# add a runtime dependency
npm install zod
# add a development tool
npm install -D vitest
npm ci deletes node_modules, installs strictly from the lockfile and fails if package.json and the lockfile disagree. That makes it both faster and safer in CI. Use npm install locally when you are changing dependencies.
Scripts
Scripts are named commands run through npm run. They are how a project exposes a consistent interface to everyone, including CI.
npm run dev
npm run build
npm test # shorthand for npm run test
Scripts can call local binaries directly, so "dev": "vite" works without a global install. They also compose: a ci script can run lint, test and build in order. Keeping these commands in package.json means nobody has to remember the exact invocation.
npx and workspaces
npx runs a package binary without a global install, which is ideal for one-off tools.
npx create-vite@latest my-app
npx eslint .
Workspaces let one repository hold several packages that share a single node_modules and lockfile.
{
"workspaces": ["packages/*", "apps/*"]
}
npm links the packages together, so a workspace can depend on a sibling without publishing it. For larger monorepos, tools like Turborepo add caching and task orchestration on top.
Security and maintenance
npm includes an audit command that checks your tree against known vulnerabilities.
npm audit
npm audit fix
npm outdated
Audit regularly and read the results rather than blindly running fix, which can pull in breaking changes. npm outdated shows which dependencies have newer versions available, which helps you plan upgrades instead of letting them pile up.
Best practices
- Commit
package-lock.jsonand never edit it by hand. - Use
npm ciin CI andnpm installonly when changing dependencies. - Keep dependencies and devDependencies separate.
- Prefer caret ranges and let the lockfile pin exact versions.
- Define scripts for every common task.
- Run
npm auditandnpm outdatedon a schedule. - Use workspaces for monorepos instead of separate repositories.
Common mistakes
- Deleting the lockfile to “fix” install problems.
- Running
npm installin CI and getting non-reproducible builds. - Putting build tools in
dependenciesand bloating runtime installs. - Ignoring audit warnings until they become urgent.
- Installing packages globally when
npxor a devDependency would do. - Editing
node_modulesand expecting the change to persist.
Where to go next
npm is the foundation of the JavaScript toolchain. Compare it with pnpm for speed and disk efficiency, understand the Node.js runtime it ships with, and add Turborepo when your repository grows into a monorepo. Then clean up a project’s scripts so the whole team shares one set of commands.