npm & Package Management
npm (Node Package Manager) is the default package manager for Node.js. It connects your project to the largest ecosystem of open-source libraries in the world. Understanding how packages, dependencies, and lock files work is essential for any JavaScript project.
npm init & package.json
Every Node.js project starts with a package.json file. It describes the project and lists its dependencies.
npm init
This walks you through creating a package.json. Use npm init -y to accept all defaults.
{
"name": "my-project",
"version": "1.0.0",
"description": "A web application",
"main": "index.js",
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "vitest",
"lint": "eslint ."
},
"dependencies": {
"svelte": "^5.0.0"
},
"devDependencies": {
"vite": "^6.0.0",
"vitest": "^2.0.0",
"eslint": "^9.0.0"
}
}
Dependencies vs devDependencies
- dependencies: packages your app needs to run in production (frameworks, utility libraries)
- devDependencies: packages needed only during development (bundlers, test frameworks, linters)
npm install svelte # Adds to dependencies
npm install -D vitest # Adds to devDependencies
npm install --save-dev eslint # Same as -D
Does It Matter?
For applications, the distinction is mostly organizational. For published libraries, it matters: consumers of your library install your dependencies but not your devDependencies.
npm install, update & audit
Installing
npm install # Install all dependencies from package.json
npm install express # Install a specific package and save it
npm install express@4 # Install a specific major version
npm uninstall express # Remove a package
Updating
npm outdated # Show packages with newer versions available
npm update # Update all packages within semver range
npm install express@latest # Update to latest, ignoring semver range
Auditing for Vulnerabilities
npm audit # Check for known vulnerabilities
npm audit fix # Auto-fix where possible
npm audit fix --force # Fix even if it requires major version bumps
Semantic Versioning
npm uses semver: MAJOR.MINOR.PATCH.
| Version | Meaning |
|---|---|
| 1.0.0 | Initial release |
| 1.0.1 | Patch: bug fix, no API changes |
| 1.1.0 | Minor: new feature, backward compatible |
| 2.0.0 | Major: breaking changes |
Range Syntax in package.json
{
"dependencies": {
"exact": "1.2.3", // Only this version
"caret": "^1.2.3", // >=1.2.3 <2.0.0 (default)
"tilde": "~1.2.3", // >=1.2.3 <1.3.0
"range": ">=1.0.0 <3.0.0"
}
}
The caret (^) is the default and is the most common. It allows minor and patch updates.
Lock Files
package-lock.json records the exact version of every installed package, including transitive dependencies.
Why It Matters
Without a lock file, npm install resolves the latest versions within the semver range. Two developers running npm install at different times could get different versions, leading to "works on my machine" bugs.
Rules
- Commit
package-lock.json: it ensures everyone gets the same versions - Never edit it manually: npm manages it
- Use
npm ciin CI/CD: installs exactly what the lock file specifies, faster thannpm install
npm ci # Clean install from lock file (CI/CD)
npm install # Resolve and update lock file (development)
npx
npx runs packages without installing them globally. It is included with npm.
# Run a package without installing
npx create-svelte my-app
npx eslint --init
npx vitest run
# Run a specific version
npx vite@5 build
When to Use npx
- One-time commands (project scaffolding, migrations)
- Running a tool without adding it to the project
- Trying a specific version of a tool
pnpm as a Faster Alternative
pnpm is a drop-in replacement for npm that uses a content-addressable store. It is faster and uses less disk space.
# Install pnpm
npm install -g pnpm
# Usage is nearly identical to npm
pnpm install
pnpm add svelte
pnpm add -D vitest
pnpm run dev
How pnpm Saves Space
npm copies packages into each project's node_modules. If 10 projects use lodash, you have 10 copies.
pnpm stores every package version once on disk and creates hard links in node_modules. Ten projects using lodash share one copy.
# npm: each project copies all packages
project-a/node_modules/lodash/ (1.2 MB)
project-b/node_modules/lodash/ (1.2 MB)
project-c/node_modules/lodash/ (1.2 MB)
# pnpm: one copy, hard-linked everywhere
~/.pnpm-store/lodash@4.17.21/ (1.2 MB, single copy)
project-a/node_modules/.pnpm/lodash@4.17.21/ (hard link)
project-b/node_modules/.pnpm/lodash@4.17.21/ (hard link)
Strict node_modules
pnpm creates a strict node_modules structure. Packages can only access their declared dependencies, not transitive ones. This catches undeclared dependency bugs that npm/Yarn miss.
Monorepos with Workspaces
Workspaces let you manage multiple packages in a single repository.
npm Workspaces
{
"name": "my-monorepo",
"workspaces": [
"packages/*",
"apps/*"
]
}
my-monorepo/
packages/
shared-utils/
package.json
ui-components/
package.json
apps/
web/
package.json
api/
package.json
package.json
Running Commands Across Workspaces
# Run tests in all workspaces
npm run test --workspaces
# Run build only in the web app
npm run build --workspace=apps/web
# Add a dependency to a specific workspace
npm install zod --workspace=packages/shared-utils
pnpm Workspaces
pnpm uses a pnpm-workspace.yaml file.
packages:
- 'packages/*'
- 'apps/*'
pnpm --filter web run build
pnpm --filter shared-utils add zod
pnpm -r run test # Run test in all packages
The node_modules Problem
A typical project can have thousands of packages in node_modules, creating deeply nested directories with hundreds of megabytes of files.
Why It Gets So Big
Each package declares its own dependencies, which have their own dependencies. A simple project might directly depend on 20 packages but transitively install 800.
How pnpm Helps
- Content-addressable store: only one copy of each package version on disk
- Hard links: no duplication across projects
- Strict resolution: prevents phantom dependencies (using packages you did not explicitly install)
Other Approaches
- Deno: no
node_modulesat all, imports from URLs - Bun: built-in package manager with symlink-based resolution
- Yarn PnP: virtual file system, no
node_modulesdirectory
Common Pitfalls
- Not committing the lock file: without
package-lock.json, builds are not reproducible. Someone on your team will get different versions. - Using
npm installin CI: usenpm ciinstead. It is faster, stricter, and installs exactly what the lock file specifies. - Installing everything as dependencies: test frameworks, linters, and build tools belong in
devDependencies. It keeps production installs smaller for libraries. - Ignoring
npm auditwarnings: vulnerabilities in your dependency tree are real. Runnpm auditregularly and fix critical issues. - Global installs: avoid
npm install -g. Use npx for one-time commands and local installs for project tools. - Phantom dependencies: if you use a package that a transitive dependency installed (but you did not declare), it will break when that transitive dependency is removed. pnpm prevents this.
Key Takeaways
package.jsonlists your project's dependencies;package-lock.jsonpins their exact versions- Always commit the lock file and use
npm ciin CI/CD for reproducible builds - Use
devDependenciesfor tools needed only during development (bundlers, linters, test frameworks) - npx runs tools without installing them globally
- pnpm solves the disk space and phantom dependency problems with a content-addressable store
- Workspaces enable monorepo management with shared dependencies across packages
- Run
npm auditregularly to catch known vulnerabilities in your dependency tree