| |

JavaScript 69 🧬 package.json + npm/yarn/pnpm

Every Node project has a package.json. It’s the manifest — the file that describes your project, lists its dependencies, and defines scripts. Around it, a package manager installs, updates, and resolves those dependencies. Three tools dominate the ecosystem: npm, yarn, and pnpm. They do the same job with different trade-offs.

Understanding package.json and how the package manager reads it is essential — because nearly every Node project you’ll touch uses this exact system, and getting it wrong means broken installs, phantom dependencies, or lockfile drift.

Key point: package.json declares what you want. The lockfile records what you actually got. The package manager resolves the space between — pinned versions, transitive dependencies, and reproducible installs.


a – What is package.json

package.json is a JSON file at the root of a Node project. It tells the package manager what your project is, what it depends on, and how to run it.

Creating one:

npm init
npm init -y

npm init -y skips the prompts and creates a minimal file with defaults.

The core fields:

{
  "name": "my-app",
  "version": "1.0.0",
  "description": "A small Node app",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "start": "node src/index.js",
    "test": "node --test"
  },
  "dependencies": {},
  "devDependencies": {},
  "engines": { "node": ">=20" },
  "license": "MIT"
}
  • name — the package name. If published, must be unique on the registry.
  • version — semver. Incremented on release.
  • main — entry point for CommonJS.
  • type"module" for ESM, "commonjs" (or omitted) for CommonJS.
  • scripts — named commands run with npm run.
  • dependencies — packages needed at runtime.
  • devDependencies — packages needed only for development.
  • engines — Node/npm version constraints.

The scripts field:

"scripts": {
  "start": "node src/index.js",
  "dev": "nodemon src/index.js",
  "build": "tsc",
  "test": "jest",
  "lint": "eslint .",
  "prepare": "husky install"
}

Run any of them with npm run <name>:

npm run dev
npm test          # shorthand for npm run test
npm start         # shorthand for npm run start

start and test are special — you can run them without run. All others need npm run.

Scripts can chain:

"scripts": {
  "lint": "eslint .",
  "test": "jest",
  "ci": "npm run lint && npm test"
}

Any shell command works — pipes, &&, environment variables, nested npm commands.

Pre and post hooks:

"scripts": {
  "prebuild": "rimraf dist",
  "build": "tsc",
  "postbuild": "cp -r public dist"
}

prebuild runs before build; postbuild runs after. Same for preinstall, posttest, and every other script.

Dependencies vs devDependencies:

FieldInstalled whenExamples
dependenciesAlwaysexpress, react, lodash
devDependenciesDevelopment onlyjest, eslint, typescript
peerDependenciesBy consumerreact for a React library
optionalDependenciesIf installableNative add-ons

Adding to them:

npm install express             # dependencies
npm install --save-dev jest     # devDependencies
npm install --save-optional fsevents

Semver — the version syntax:

Versions are MAJOR.MINOR.PATCH. A range tells npm what’s acceptable.

RangeMeaning
1.2.3Exactly 1.2.3
^1.2.3≥1.2.3, <2.0.0
~1.2.3≥1.2.3, <1.3.0
>=1.2.31.2.3 or higher
*Any version
latestLatest published

The ^ symbol is the default. It allows patch and minor updates but not major ones — the assumption being that major versions may break compatibility.

The main, module, exports fields:

{
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    },
    "./utils": "./dist/utils.mjs"
  }
}
  • main — CommonJS entry
  • module — ESM entry (bundler hint)
  • exports — modern, explicit entry points for different conditions

The exports field is the modern standard — it lets a package expose different files for import vs require and restrict what consumers can access.

Other fields worth knowing:

FieldPurpose
privateSet true to prevent publishing
filesWhitelist of files to publish
workspacesMonorepo package locations
binCLI executables
repositoryGit URL
keywordsFor npm search
authorMaintainer info

A private package:

{
  "private": true
}

Prevents accidental npm publish. Essential for internal projects.

The bin field — CLI executables:

{
  "bin": {
    "mycli": "./bin/cli.js"
  }
}

After npm install -g, users can run mycli.


b – The three package managers

Three tools dominate: npm, yarn, and pnpm. All read package.json, all produce a lockfile, all install from the npm registry. They differ in speed, disk usage, and dependency resolution.

npm — the default:

Ships with Node. The original. Stable and universally compatible.

npm install
npm install express
npm install --save-dev jest
npm update
npm uninstall express
npm run build

yarn — the fast alternative:

Facebook’s response to npm’s early performance issues. Introduced yarn.lock and Plug’n’Play. Still popular, though less differentiated now that npm caught up.

yarn
yarn add express
yarn add --dev jest
yarn remove express
yarn build

pnpm — the disk-efficient one:

Uses a global content-addressable store and hard links to avoid duplicating packages across projects. Faster installs and lower disk usage. Increasingly popular for monorepos and CI.

pnpm install
pnpm add express
pnpm add -D jest
pnpm remove express
pnpm build

Comparing the three:

Featurenpmyarnpnpm
Ships with Node
Lockfilepackage-lock.jsonyarn.lockpnpm-lock.yaml
Install speedGoodGoodFastest
Disk usageHigherHigherLowest
Phantom deps
Monorepo supportWorkspacesWorkspacesWorkspaces
Global storePartial
Native speedSlowFastFast

The phantom dependency problem:

npm and yarn hoist dependencies into a flat node_modules, which lets you require packages you didn’t declare. This works until it doesn’t — a transitive dependency updates, and your undeclared import breaks.

pnpm’s strict layout prevents this: only declared dependencies are importable. It catches bugs early.

Lockfiles:

Every manager writes a lockfile. It records the exact versions installed, including transitive dependencies.

ManagerLockfile
npmpackage-lock.json
yarnyarn.lock
pnpmpnpm-lock.yaml

Always commit the lockfile. It ensures everyone on the team — and CI — installs the same versions. Without it, ^1.2.3 can install different code on different days.

Installing from a lockfile:

npm ci         # clean install from lockfile
yarn install --frozen-lockfile
pnpm install --frozen-lockfile

npm ci is faster than npm install, fails if the lockfile is out of sync, and doesn’t modify package.json. Use it in CI.

The difference between install and ci:

CommandReadsWrites lockfileUse
npm installpackage.jsonLocal development
npm cipackage-lock.jsonCI, production

Installing specific versions:

npm install express@4.18.2
npm install express@^4
npm install express@latest
npm install express@next

Semver ranges, exact versions, tags — all work.

Updating packages:

npm update
npm outdated
npm install express@latest

outdated lists packages with newer versions. update brings them within the allowed range. To upgrade majors, install the new version explicitly.

Auditing for vulnerabilities:

npm audit
npm audit fix

audit scans for known CVEs in the dependency tree. fix attempts automatic upgrades.

Global vs local installs:

npm install -g typescript    # global
npm install typescript       # local to project

Always prefer local. Global installs pollute the system and cause version drift across projects.

Running without installing:

npx create-react-app my-app
npx cowsay hello

npx downloads a package temporarily, runs it, and cleans up. Useful for one-off CLI tools.

pnpm — why it saves space:

Instead of copying packages into every project, pnpm stores them once in a global content-addressable store (~/.pnpm-store) and hard-links them into node_modules. Ten projects using React 18 share the same bytes on disk.

The result: installs are faster, disk usage is minimal, and updates are near-instant.

Workspaces — for monorepos:

All three support workspaces — multiple packages in one repo.

npm:

{
  "workspaces": ["packages/*"]
}

pnpm:

# pnpm-workspace.yaml
packages:
  - 'packages/*'

Workspaces let packages reference each other without publishing. Dependencies resolve locally during development.

Choosing a manager:

SituationRecommendation
Simplest setupnpm
Existing yarn projectStay on yarn
Monorepopnpm
Disk-constrained environmentpnpm
Maximum compatibilitynpm
Team already using oneUse that one

The most important thing is consistency — everyone on the team uses the same manager with the same lockfile.


c – Common workflows and pitfalls

Day-to-day usage and the mistakes to avoid.

Installing a project:

git clone https://github.com/user/project.git
cd project
npm ci
npm run dev

npm ci — clean, fast, lockfile-based. Faster and safer than npm install for a fresh checkout.

Adding a dependency:

npm install axios

Adds to package.json and updates the lockfile. Commit both.

Removing a dependency:

npm uninstall axios

Removes from both files.

Running scripts:

npm run build
npm test
npm start

The npx shortcut:

npx tsc --init
npx prettier --write .

No install needed — good for one-off tools.

Passing arguments through scripts:

npm run test -- --watch

The -- separates npm’s arguments from the script’s. Everything after goes to the underlying command.

Environment variables in scripts:

"scripts": {
  "start": "NODE_ENV=production node src/index.js"
}

For cross-platform env vars, use cross-env.

Running scripts in parallel:

"scripts": {
  "dev": "concurrently \"npm:server\" \"npm:client\"",
  "server": "node server.js",
  "client": "vite"
}

Tools like concurrently and npm-run-all orchestrate multi-process scripts.

The postinstall script:

"scripts": {
  "postinstall": "husky install"
}

Runs automatically after every npm install. Useful for setup, but dangerous if it does heavy work or fails.

Peer dependencies:

A package can declare that it expects a host package — like react for a React component library:

"peerDependencies": {
  "react": ">=17"
}

npm 7+ installs peer deps automatically. Older npm required manual installs and warned loudly.

Common pitfalls:

1. Committing node_modules. Never do it. It’s large, platform-specific, and reproducible from the lockfile. Add it to .gitignore.

2. Not committing the lockfile. Everyone gets different versions. Breaks the “works on my machine” promise.

3. Mixing package managers. Running yarn add in a project with package-lock.json produces two lockfiles. Pick one and stick to it.

4. Phantom dependencies. Importing a package you didn’t declare. It works until a transitive dependency removes it. Use pnpm or verify all imports are in package.json.

5. Ignoring engines. Node versions differ; some packages need v18+, others v20+. Set engines and enforce it in CI.

6. Breaking semver. Publishing a breaking change under a minor version. Consumers get unexpected failures.

7. Publishing secrets. Setting "private": false on a project with API keys. Use "private": true for internal projects.

8. Not using npm ci in CI. npm install can mutate the lockfile and install newer versions. Use npm ci or --frozen-lockfile.

9. Large node_modules. Using npm or yarn on a big monorepo can produce gigabytes. pnpm fixes this.

10. Upgrading everything at once. npm update can break many things. Upgrade one major at a time, run tests, commit.

Checking for outdated packages:

npm outdated

Output shows current, wanted, and latest versions. Decide what to upgrade manually.

Auditing:

npm audit

Lists vulnerabilities with severity and patch suggestions. npm audit fix applies safe fixes; --force applies breaking ones.

Listing installed packages:

npm list
npm list --depth=0

--depth=0 shows only top-level dependencies.

Verifying the tree:

npm ls

Fails if there are unmet peer dependencies or missing packages.

A production install:

npm ci --omit=dev

Installs only dependencies, skipping devDependencies. Used in Docker images and production deploys.

A complete package.json for a modern Node app:

{
  "name": "my-app",
  "version": "1.0.0",
  "type": "module",
  "private": true,
  "engines": { "node": ">=20" },
  "scripts": {
    "start": "node src/index.js",
    "dev": "node --watch src/index.js",
    "test": "node --test",
    "lint": "eslint .",
    "format": "prettier --write .",
    "ci": "npm run lint && npm test"
  },
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "eslint": "^8.50.0",
    "prettier": "^3.0.0"
  }
}

Each field serves a purpose: type enables ESM, private prevents publishing, engines pins Node, scripts define the workflow, and dependencies are separated by role.


Complete Example Session

# ============================================
# PART 1: INITIALIZE
# ============================================

npm init -y

# ============================================
# PART 2: VIEW PACKAGE.JSON
# ============================================

cat package.json

# ============================================
# PART 3: ADD DEPENDENCIES
# ============================================

npm install express
npm install --save-dev jest

# ============================================
# PART 4: RUN SCRIPTS
# ============================================

npm run build
npm test

# ============================================
# PART 5: INSTALL FROM LOCKFILE
# ============================================

npm ci

# ============================================
# PART 6: UPDATE PACKAGES
# ============================================

npm outdated
npm update

# ============================================
# PART 7: AUDIT
# ============================================

npm audit
npm audit fix

# ============================================
# PART 8: UNINSTALL
# ============================================

npm uninstall express

# ============================================
# PART 9: NPX
# ============================================

npx tsc --init
npx prettier --write .

# ============================================
# PART 10: PASS ARGS TO SCRIPT
# ============================================

npm run test -- --watch

# ============================================
# PART 11: LIST PACKAGES
# ============================================

npm list --depth=0

# ============================================
# PART 12: PRODUCTION INSTALL
# ============================================

npm ci --omit=dev

# ============================================
# PART 13: YARN
# ============================================

yarn
yarn add express
yarn add --dev jest
yarn build

# ============================================
# PART 14: PNPM
# ============================================

pnpm install
pnpm add express
pnpm add -D jest
pnpm build

# ============================================
# PART 15: FROZEN LOCKFILE (CI)
# ============================================

pnpm install --frozen-lockfile

# ============================================
# PART 16: TYPICAL .gitignore
# ============================================

cat .gitignore
# node_modules/
# dist/
# .env
# *.log

Quick Reference

package.json Fields

FieldPurpose
namePackage name
versionSemver
type"module" or "commonjs"
mainCommonJS entry
moduleESM entry
exportsConditional exports
scriptsnpm commands
dependenciesRuntime deps
devDependenciesDev deps
peerDependenciesHost requirements
enginesNode/npm versions
privatePrevent publish
binCLI executables
filesPublished files
workspacesMonorepo packages

Semver Ranges

RangeAllows
1.2.3Exact
^1.2.3Minor + patch
~1.2.3Patch only
>=1.2.3Higher
*Any
latestLatest

npm Commands

CommandPurpose
npm init -yCreate package.json
npm installInstall from package.json
npm ciInstall from lockfile
npm install PKGAdd dependency
npm install -D PKGAdd dev dependency
npm uninstall PKGRemove
npm updateUpdate within ranges
npm outdatedList newer versions
npm run SCRIPTRun a script
npm auditSecurity scan
npm listShow tree
npx PKGRun without install

yarn Commands

CommandPurpose
yarnInstall
yarn add PKGAdd
yarn add -D PKGAdd dev
yarn remove PKGRemove
yarn run SCRIPTRun script
yarn install --frozen-lockfileCI install

pnpm Commands

CommandPurpose
pnpm installInstall
pnpm add PKGAdd
pnpm add -D PKGAdd dev
pnpm remove PKGRemove
pnpm run SCRIPTRun script
pnpm install --frozen-lockfileCI install

Lockfiles

ManagerFile
npmpackage-lock.json
yarnyarn.lock
pnpmpnpm-lock.yaml

Manager Comparison

Aspectnpmyarnpnpm
SpeedGoodGoodFastest
DiskHighHighLow
Phantom depsYesYesNo
MonorepoOKGoodBest
Ships with Node

Dependency Types

TypeInstalledPublished
dependenciesAlwaysConsumers
devDependenciesDev onlyNever
peerDependenciesConsumerDeclared
optionalDependenciesIf possibleConsumers

Install vs ci

Aspectinstallci
Readspackage.jsonlockfile
Writes lockfile
Fails on mismatch
SpeedSlowerFaster
UseDevelopmentCI/CD

Best Practices

Do This:

# Commit the lockfile
git add package-lock.json                     # ✅

# Use npm ci in CI
npm ci                                        # ✅

# Ignore node_modules
echo "node_modules/" >> .gitignore            # ✅

# Pin Node version
"engines": { "node": ">=20" }                 # ✅

# Mark private
"private": true                               # ✅

# Separate deps from devDeps
npm install --save-dev jest                   # ✅

# Use npx for one-off tools
npx tsc --init                                # ✅

# Upgrade one major at a time
npm install express@5                         # ✅

# Choose one manager
pnpm install                                  # ✅ stick with it

# Use pnpm for monorepos
pnpm install                                  # ✅

Don’t Do This:

# Don't commit node_modules
git add node_modules                          # ❌

# Don't mix package managers
yarn add express                              # ❌ with package-lock.json

# Don't use npm install in CI
npm install                                   # ❌ use npm ci

# Don't ignore the lockfile
# (skip committing it)                        # ❌

# Don't publish internal packages
"private": false                              # ❌ for private apps

# Don't commit secrets
.env                                          # ❌ gitignore it

# Don't run postinstall with heavy work
"postinstall": "webpack"                      # ⚠️  slow installs

# Don't use `*` in version ranges
"express": "*"                                # ❌ unreproducible

# Don't upgrade everything at once
npm update --force                            # ❌ breaks things

# Don't run global installs casually
npm install -g typescript                     # ⚠️  prefer local

Common Pitfalls

PitfallProblemSolution
Committing node_modulesHuge repo.gitignore
No lockfile in gitVersion driftCommit lockfile
Mixing managersTwo lockfilesPick one
npm install in CINon-deterministicnpm ci
Phantom depsBreaks on updatespnpm or audit
No enginesWrong NodeSet + enforce
* versionsAnything installsPin ranges
Global installsVersion driftPrefer local
Publishing secretsLeak"private": true
Upgrading all at onceWidespread breakageOne at a time

Real-World Examples

1. New project

npm init -y

Creates a default package.json.

2. Add runtime dependency

npm install express

Adds to dependencies and the lockfile.

3. Add dev dependency

npm install --save-dev jest

Adds to devDependencies — not installed in production.

4. Fresh clone

npm ci

Clean install from the lockfile. CI-safe.

5. Add a script

"scripts": {
  "dev": "node --watch src/index.js"
}

Run with npm run dev.

6. Run with extra args

npm test -- --watch

Everything after -- goes to the underlying command.

7. Check outdated

npm outdated

Shows current, wanted, and latest for each package.

8. Audit

npm audit fix

Applies safe security upgrades.

9. One-off tool

npx prettier --write .

Runs without a global install.

10. Production install

npm ci --omit=dev

Only runtime dependencies.


Visual: package.json vs node_modules

┌──────────────────────────────────────────────┐
│  package.json                                │
│                                              │
│  {                                           │
│    "dependencies": {                         │
│      "express": "^4.18.2"                    │
│    }                                         │
│  }                                           │
│                                              │
│  Declares what you want                      │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  node_modules/                               │
│                                              │
│  express/                                    │
│  ├── package.json                            │
│  ├── index.js                                │
│  └── node_modules/                           │
│      └── ...                                 │
│                                              │
│  Contains what you actually got              │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  package-lock.json                           │
│                                              │
│  Pins exact versions of everything           │
│  including transitive dependencies           │
│                                              │
└──────────────────────────────────────────────┘

Visual: npm install vs npm ci

┌──────────────────────────────────────────────┐
│  npm install                                 │
│                                              │
│  Reads package.json                          │
│  May update lockfile                         │
│  Installs latest matching versions           │
│  Good for development                        │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  npm ci                                      │
│                                              │
│  Reads package-lock.json                     │
│  Never writes lockfile                       │
│  Fails if package.json and lock mismatch     │
│  Faster, deterministic                       │
│  Good for CI/CD                              │
│                                              │
└──────────────────────────────────────────────┘

Visual: Hoisted vs Symlinked

┌──────────────────────────────────────────────┐
│  npm / yarn (hoisted)                        │
│                                              │
│  node_modules/                               │
│  ├── express/                                │
│  ├── lodash/          ← transitive deps      │
│  ├── body-parser/     ← visible even if      │
│  └── cookie/          ← not in package.json  │
│                                              │
│  ⚠️  phantom deps possible                   │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  pnpm (strict symlinks)                      │
│                                              │
│  node_modules/                               │
│  ├── express/  → symlink to global store     │
│  └── .pnpm/    → real packages               │
│                                              │
│  Only declared deps are importable           │
│  ✅ no phantom deps                          │
│                                              │
└──────────────────────────────────────────────┘

Summary

ConceptField / ToolPurpose
Manifestpackage.jsonDeclares project
ScriptsscriptsNamed commands
Runtime depsdependenciesNeeded always
Dev depsdevDependenciesNeeded in dev
Peer depspeerDependenciesHost-provided
Entrymain / module / exportsImport resolution
ESMtypeModule system
Version pinenginesNode/npm versions
PrivateprivatePrevent publish
Lockfilepackage-lock.jsonExact versions
Installnpm installFrom manifest
Clean installnpm ciFrom lockfile
Runnpm runExecute script
Addnpm installAdd dep
Removenpm uninstallRemove dep
Checknpm outdatedNewer versions
Auditnpm auditSecurity
One-offnpxTemporary run
Managernpm / yarn / pnpmChoose one

Key takeaways:

  • package.json is the manifest — name, version, scripts, dependencies
  • scripts define repeatable commands — run with npm run
  • dependencies are for runtime; devDependencies for development
  • Use semver ranges^1.2.3 for compatible upgrades, no *
  • type: "module" enables ESM; omit for CommonJS
  • private: true prevents accidental publishing
  • engines pins the Node version — enforce it in CI
  • Lockfiles record exact versions — always commit them
  • npm ci is for CI — fast, deterministic, fails on mismatch
  • npm install is for development — may update the lockfile
  • npx runs one-off tools without installing globally
  • npm, yarn, and pnpm all do the same job — pick one and stick to it
  • pnpm saves disk and prevents phantom dependencies
  • Never commit node_modules — add it to .gitignore
  • Never mix package managers — two lockfiles, two behaviors
  • Upgrade one major at a time — mass updates break things
  • Use --omit=dev for production installs — smaller, faster, safer

Remember: package.json is the contract between your project and its dependencies. The lockfile is the receipt. The package manager is the courier. Keep them in sync, commit the lockfile, and pick a single manager. Use npm ci in CI, npm install locally, and npx for one-off tools. Understand semver, keep dependencies and devDependencies separate, and never commit node_modules. Master this system, and every Node project behaves the same way — from a weekend script to a production monorepo.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!