πŸ“ Tutorials
Β· 7 min read

npm Complete Guide: Package Management for Node.js


npm is the default package manager for Node.js and the world’s largest software registry. Whether you’re installing a single dependency or managing a monorepo with workspaces, npm is the tool you’ll use daily. This guide covers everything from basics to advanced workflows.

For quick reference, keep the npm cheat sheet open. If you’re deciding between package managers, check our npm vs pnpm vs yarn comparison.

Installing Node.js and npm

npm comes bundled with Node.js. The recommended way to install is through a version manager:

# Using nvm (recommended)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
nvm install --lts
nvm use --lts

# Verify
node --version
npm --version

Using nvm lets you switch between Node versions per project. Create a .nvmrc file in your project root:

20.11.0

Then nvm use automatically picks the right version.

Package.json Essentials

Every Node.js project starts with package.json:

npm init -y  # creates package.json with defaults

Key fields:

{
  "name": "my-app",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "node --watch src/index.js",
    "build": "tsc",
    "start": "node dist/index.js",
    "test": "vitest"
  },
  "dependencies": {
    "express": "^4.18.0"
  },
  "devDependencies": {
    "typescript": "^5.4.0"
  }
}

The type: "module" field enables ES modules (import/export) instead of CommonJS (require).

Installing Packages

# Add a dependency
npm install express

# Add a dev dependency
npm install -D typescript vitest

# Install a specific version
npm install react@18.2.0

# Install all dependencies from package.json
npm install

# Install globally (for CLI tools)
npm install -g vercel

Version Ranges

  • ^4.18.0 β€” compatible with 4.x.x (most common, default)
  • ~4.18.0 β€” patch updates only: 4.18.x
  • 4.18.0 β€” exact version, no updates
  • * β€” any version (dangerous, avoid)

The Lock File

package-lock.json pins exact versions of every dependency (including transitive ones). Always commit it:

# Regenerate lock file
rm package-lock.json && npm install

# Install exactly what's in the lock file (CI/CD)
npm ci

npm ci is faster than npm install for CI because it skips resolution and installs directly from the lock file.

npm Scripts

Scripts are the standard way to define project commands:

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint .",
    "format": "prettier --write .",
    "typecheck": "tsc --noEmit",
    "precommit": "npm run lint && npm run typecheck"
  }
}

Run them with npm run <name>. Special scripts start, test, and stop don’t need run:

npm start     # same as npm run start
npm test      # same as npm run test
npm run dev   # custom scripts need 'run'

Pre/Post Hooks

{
  "scripts": {
    "prebuild": "rm -rf dist",
    "build": "tsc",
    "postbuild": "cp package.json dist/"
  }
}

prebuild runs automatically before build, and postbuild runs after.

Workspaces (Monorepos)

npm workspaces let you manage multiple packages in one repo:

{
  "workspaces": ["packages/*", "apps/*"]
}
my-monorepo/
β”œβ”€β”€ package.json
β”œβ”€β”€ packages/
β”‚   β”œβ”€β”€ shared/
β”‚   β”‚   └── package.json
β”‚   └── ui/
β”‚       └── package.json
└── apps/
    └── web/
        └── package.json

Commands:

# Install all workspace dependencies
npm install

# Run a script in a specific workspace
npm run build -w packages/shared

# Run a script in all workspaces
npm run build --workspaces

# Add a dependency to a specific workspace
npm install zod -w apps/web

Updating Dependencies

# Check for outdated packages
npm outdated

# Update within version ranges
npm update

# Update a specific package
npm install express@latest

# Interactive update tool
npx npm-check-updates -i

For major version bumps, use npm-check-updates:

npx npm-check-updates -u  # updates package.json
npm install                # installs new versions

Security

# Check for known vulnerabilities
npm audit

# Auto-fix what's possible
npm audit fix

# Force fix (may include breaking changes)
npm audit fix --force

Publishing Packages

# Login to npm
npm login

# Publish (public by default for unscoped packages)
npm publish

# Publish a scoped package publicly
npm publish --access public

# Bump version and publish
npm version patch  # 1.0.0 β†’ 1.0.1
npm version minor  # 1.0.0 β†’ 1.1.0
npm version major  # 1.0.0 β†’ 2.0.0
npm publish

Use .npmignore or the files field in package.json to control what gets published.

Troubleshooting Common Errors

npm errors are notoriously cryptic. Here are the most common ones with links to detailed fixes:

The nuclear option for most npm issues:

rm -rf node_modules package-lock.json
npm cache clean --force
npm install

npx β€” Run Packages Without Installing

npx runs a package’s binary without installing it globally:

# Run a one-off command
npx create-next-app my-app
npx astro add tailwind
npx tsc --init

# Run a specific version
npx tsx@latest script.ts

# Run from a GitHub repo
npx github:user/repo

npx first checks if the package is installed locally, then globally, then downloads it temporarily. It’s perfect for scaffolding tools and one-off commands.

.npmrc Configuration

Project-level npm config goes in .npmrc:

# Use exact versions by default
save-exact=true

# Set a default registry (for private packages)
registry=https://registry.npmjs.org/

# Scoped registry
@mycompany:registry=https://npm.mycompany.com/

# Auto-install peer dependencies
legacy-peer-deps=false

# Engine strictness
engine-strict=true

Engines and Node Version

Lock down which Node.js versions your project supports:

{
  "engines": {
    "node": ">=20.0.0",
    "npm": ">=10.0.0"
  }
}

Combined with engine-strict=true in .npmrc, npm will refuse to install if the Node version doesn’t match.

Overrides (Dependency Patching)

Force a specific version of a transitive dependency:

{
  "overrides": {
    "lodash": "4.17.21",
    "some-package>vulnerable-dep": "2.0.0"
  }
}

This is useful for fixing security vulnerabilities in dependencies you don’t control directly.

Private Packages and Scopes

Scoped packages are namespaced under an organization:

# Create a scoped package
npm init --scope=@mycompany

# Publish privately (requires npm paid plan)
npm publish

# Publish scoped package publicly
npm publish --access public

For private registries (Artifactory, Verdaccio, GitHub Packages):

# .npmrc
@mycompany:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}

Lifecycle Hooks and Custom Scripts

npm supports lifecycle hooks that run at specific points:

{
  "scripts": {
    "prepare": "husky install",
    "preinstall": "npx only-allow pnpm",
    "postinstall": "prisma generate",
    "prepack": "npm run build",
    "prepublishOnly": "npm run test && npm run build"
  }
}

Key hooks:

  • prepare β€” runs after npm install and before npm publish (great for build steps and git hooks)
  • preinstall β€” runs before installing dependencies (useful to enforce a specific package manager)
  • postinstall β€” runs after all dependencies are installed (code generation, native builds)
  • prepublishOnly β€” runs before npm publish (validation, tests)

Environment Variables in Scripts

Pass environment variables to npm scripts:

{
  "scripts": {
    "dev": "NODE_ENV=development node src/index.js",
    "build": "NODE_ENV=production node build.js",
    "start:staging": "PORT=4000 node dist/index.js"
  }
}

For cross-platform compatibility (Windows doesn’t support inline env vars), use cross-env:

{
  "scripts": {
    "build": "cross-env NODE_ENV=production node build.js"
  }
}

Debugging npm Issues

When things go wrong:

# Verbose output
npm install --verbose

# Check where npm looks for packages
npm root
npm root -g

# See the full dependency tree
npm ls
npm ls express

# Check why a package is installed
npm explain express

# See npm's config
npm config list

Common Mistakes

Not committing the lock file. package-lock.json ensures everyone gets the same dependency versions. Without it, npm install might resolve different versions on different machines.

Using npm install in CI. Use npm ci instead β€” it’s faster, stricter, and installs exactly what’s in the lock file. npm install can modify the lock file.

Installing everything globally. Global installs (npm install -g) should only be for CLI tools you use across projects. For project dependencies, always install locally.

Ignoring peer dependency warnings. Peer dependency mismatches cause subtle bugs. Fix them by installing the correct version or using overrides to force a specific version.

Not using --save-exact. Version ranges (^4.18.0) can lead to different versions across installs. For maximum reproducibility, use npm install --save-exact or set save-exact=true in .npmrc.

npm vs Alternatives

npm isn’t the only option. pnpm uses a content-addressable store that saves disk space and is stricter about dependencies. Yarn offers plug’n’play for zero-install setups. Bun has its own package manager that’s significantly faster.

For a detailed comparison, see npm vs pnpm vs yarn and pnpm vs npm.

Next Steps

  • Grab the npm cheat sheet for daily reference
  • Explore pnpm if you want stricter dependency management
  • Check out Deno for a modern Node.js alternative
  • Learn TypeScript to add type safety to your Node.js projects