Node.js Package Management
What Is a Package Manager?
A tool for installing, managing, and sharing external libraries (packages) in the Node.js ecosystem. Package metadata is recorded in package.json, and the actual code is stored in node_modules/.
npm vs yarn vs pnpm Comparison
| Feature | npm | yarn (v1 Classic) | pnpm |
|---|---|---|---|
| Released | 2010 | 2016 | 2017 |
| Bundled | With Node.js | Separate install | Separate install |
| Install speed | Average | Fast (parallel) | Very fast |
| Disk usage | High | High | Low (symlinks) |
| Workspaces | Supported | Supported | Supported (excellent) |
| Security | Average | Stricter | Strict |
| Lock file | package-lock.json | yarn.lock | pnpm-lock.yaml |
Command Comparison
# Install a package
npm install express
yarn add express
pnpm add express
# Dev dependency
npm install --save-dev jest
yarn add --dev jest
pnpm add -D jest
# Install all dependencies (CI)
npm ci
yarn install --frozen-lockfile
pnpm install --frozen-lockfile
# Run a script
npm run build
yarn build
pnpm build
# Global install
npm install -g pm2
yarn global add pm2
pnpm add -g pm2
# Remove a package
npm uninstall express
yarn remove express
pnpm remove express
# Update
npm update
yarn upgrade
pnpm update
How pnpm Saves Disk Space
npm/yarn approach:
project-a/node_modules/lodash@4.17.21/ (copy)
project-b/node_modules/lodash@4.17.21/ (copy)
project-c/node_modules/lodash@4.17.21/ (copy)
pnpm approach:
~/.pnpm-store/lodash@4.17.21/ (stored only once)
project-a/node_modules/lodash → symbolic link
project-b/node_modules/lodash → symbolic link
project-c/node_modules/lodash → symbolic link
package.json Fields Explained
{
"name": "my-awesome-app",
"version": "1.2.3",
"description": "Node.js application example",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"exports": {
".": {
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js"
},
"./utils": "./dist/utils.js"
},
"type": "module",
"scripts": {
"start": "node dist/index.js",
"dev": "nodemon src/index.js",
"build": "tsc",
"test": "jest --coverage",
"test:watch": "jest --watch",
"lint": "eslint src/**/*.js",
"lint:fix": "eslint src/**/*.js --fix",
"format": "prettier --write src/**/*.js",
"prepare": "husky install",
"prepublishOnly": "npm run build && npm test"
},
"keywords": ["node", "express", "api"],
"author": {
"name": "John Doe",
"email": "john@example.com",
"url": "https://example.com"
},
"license": "MIT",
"homepage": "https://github.com/user/repo#readme",
"repository": {
"type": "git",
"url": "https://github.com/user/repo.git"
},
"bugs": {
"url": "https://github.com/user/repo/issues"
},
"engines": {
"node": ">=20.0.0",
"npm": ">=10.0.0"
},
"dependencies": {
"express": "^4.18.2",
"dotenv": "^16.3.1"
},
"devDependencies": {
"jest": "^29.7.0",
"nodemon": "^3.0.2",
"eslint": "^8.55.0"
},
"peerDependencies": {
"react": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "^2.3.3"
},
"files": [
"dist",
"README.md"
],
"private": true
}
Key Field Descriptions
name: Package name, must be unique in the npm registry (scopes allowed:@myorg/pkg)version: SemVer version (major.minor.patch)main: CommonJSrequire()entry pointmodule: ESMimportentry point (for bundlers)exports: Precise entry point control (Node.js 12+)type: When set to"module",.jsfiles are treated as ESMfiles: List of files to include when publishing to npmprivate: Whentrue, prevents accidental publishing
Dependency Types
{
"dependencies": {
"express": "^4.18.2"
},
"devDependencies": {
"jest": "^29.7.0",
"nodemon": "^3.0.2",
"typescript": "^5.3.2",
"eslint": "^8.55.0"
},
"peerDependencies": {
"react": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "^2.3.3"
}
}
| Type | Install condition | Purpose |
|---|---|---|
dependencies | Always | Packages required at runtime |
devDependencies | npm install (excluded with --production) | Development/build tools |
peerDependencies | Manual (user installs) | Declares compatible versions when authoring a library |
optionalDependencies | Ignored on failure | Platform-specific optional dependencies |
# Install production only (excluding devDependencies)
npm install --production
# or NODE_ENV=production npm install
Version Range Specifiers
SemVer (Semantic Versioning): major.minor.patch
- major: Incompatible API changes (breaking change)
- minor: Backwards-compatible new features
- patch: Bug fixes
{
"dependencies": {
"pkg-a": "4.18.2", // Exactly this version
"pkg-b": "^4.18.2", // 4.x.x (major fixed)
"pkg-c": "~4.18.2", // 4.18.x (minor fixed)
"pkg-d": ">=4.0.0", // 4.0.0 or higher
"pkg-e": ">=4.0.0 <5.0.0",// 4.x.x range
"pkg-f": "*", // Latest version (not recommended)
"pkg-g": "latest", // Latest version tag
"pkg-h": "4.x", // 4.x.x
"pkg-i": "git+https://github.com/user/repo.git", // Git URL
"pkg-j": "file:../local-package" // Local path
}
}
Recommended Update Policy
# Check currently installed versions
npm list --depth=0
# Check for updatable packages
npm outdated
# Safe updates (patch, minor)
npm update
# Major update (use with caution)
npm install express@latest
# Check for vulnerabilities
npm audit
# Auto-fix
npm audit fix
The Role of package-lock.json and yarn.lock
package.json → "Roughly this version is fine" (range specifier)
package-lock.json → "Use exactly this version" (pinned)
package-lock.json Example
{
"name": "my-app",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-...",
"dependencies": {
"accepts": "~1.3.8",
"body-parser": "1.20.1"
}
}
}
}
Important rules:
package-lock.jsonmust always be committed to gitnode_modules/must be added to.gitignore- Use
npm ciin CI/CD (strictly follows the lock file)
Using npm Scripts
{
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js --watch src",
"build": "tsc -p tsconfig.json",
"clean": "rm -rf dist",
"prebuild": "npm run clean",
"postbuild": "echo Build complete",
"test": "jest",
"test:ci": "jest --ci --coverage --forceExit",
"lint": "eslint src",
"format": "prettier --write 'src/**/*.{js,ts}'",
"typecheck": "tsc --noEmit",
"check": "npm run lint && npm run typecheck && npm test",
"docker:build": "docker build -t myapp .",
"docker:run": "docker run -p 3000:3000 myapp",
"db:migrate": "node scripts/migrate.js",
"db:seed": "node scripts/seed.js",
"db:reset": "npm run db:migrate && npm run db:seed"
}
}
Special Scripts
# pre/post hooks
# prebuild → build → postbuild run automatically in order
# Passing environment variables
NODE_ENV=production npm run build
# Passing arguments to a script
npm run test -- --verbose --testNamePattern="User"
# jest --verbose --testNamePattern="User"
# Running local packages with npx
npx jest
npx ts-node src/index.ts
Local vs Global Packages
# Local install (project dependency)
npm install express
# → stored in node_modules/express/
# → available only within the project
# Global install (system tool)
npm install -g pm2 nodemon typescript
# → stored in global path (check with: which pm2)
# → usable as CLI from anywhere
# Check globally installed packages
npm list -g --depth=0
# Global install path
npm root -g
npm bin -g
# Recommended: use npx instead of global install
npx create-next-app my-app
npx nodemon server.js
.npmrc Configuration
# .npmrc (project root or home directory)
# Registry setting
registry=https://registry.npmjs.org/
# Per-scope registry (private registry)
@mycompany:registry=https://npm.mycompany.com/
# Log level
loglevel=warn
# Save behavior
save-exact=true # Save exact version without ^
save-prefix=~ # Use ~ instead of ^
# Cache path
cache=/tmp/npm-cache
# Proxy settings (corporate environment)
# proxy=http://proxy.company.com:8080
# https-proxy=http://proxy.company.com:8080
# Package lock
package-lock=true
# Offline mode
# offline=true
# Auth token (GitHub Packages, etc.)
# //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
Pro Tips
workspaces (Monorepo)
// Root package.json
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"packages/*",
"apps/*"
]
}
# Install a package in a specific workspace
npm install express --workspace=apps/api
# Run a script in a specific workspace
npm run build --workspace=packages/ui
# Run a script in all workspaces
npm run build --workspaces
Package Security Audit
# Check for vulnerabilities
npm audit
# Output in JSON format
npm audit --json
# Auto-fix patchable vulnerabilities
npm audit fix
# Force fix (caution: includes major updates)
npm audit fix --force
# Ignore a specific package vulnerability (temporary)
# Configure in .nsprc or .npmrc