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:
| Field | Installed when | Examples |
|---|---|---|
dependencies | Always | express, react, lodash |
devDependencies | Development only | jest, eslint, typescript |
peerDependencies | By consumer | react for a React library |
optionalDependencies | If installable | Native 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.
| Range | Meaning |
|---|---|
1.2.3 | Exactly 1.2.3 |
^1.2.3 | ≥1.2.3, <2.0.0 |
~1.2.3 | ≥1.2.3, <1.3.0 |
>=1.2.3 | 1.2.3 or higher |
* | Any version |
latest | Latest 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:
| Field | Purpose |
|---|---|
private | Set true to prevent publishing |
files | Whitelist of files to publish |
workspaces | Monorepo package locations |
bin | CLI executables |
repository | Git URL |
keywords | For npm search |
author | Maintainer 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:
| Feature | npm | yarn | pnpm |
|---|---|---|---|
| Ships with Node | ✅ | ❌ | ❌ |
| Lockfile | package-lock.json | yarn.lock | pnpm-lock.yaml |
| Install speed | Good | Good | Fastest |
| Disk usage | Higher | Higher | Lowest |
| Phantom deps | ✅ | ✅ | ❌ |
| Monorepo support | Workspaces | Workspaces | Workspaces |
| Global store | ❌ | Partial | ✅ |
| Native speed | Slow | Fast | Fast |
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.
| Manager | Lockfile |
|---|---|
| npm | package-lock.json |
| yarn | yarn.lock |
| pnpm | pnpm-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:
| Command | Reads | Writes lockfile | Use |
|---|---|---|---|
npm install | package.json | ✅ | Local development |
npm ci | package-lock.json | ❌ | CI, 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:
| Situation | Recommendation |
|---|---|
| Simplest setup | npm |
| Existing yarn project | Stay on yarn |
| Monorepo | pnpm |
| Disk-constrained environment | pnpm |
| Maximum compatibility | npm |
| Team already using one | Use 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
| Field | Purpose |
|---|---|
name | Package name |
version | Semver |
type | "module" or "commonjs" |
main | CommonJS entry |
module | ESM entry |
exports | Conditional exports |
scripts | npm commands |
dependencies | Runtime deps |
devDependencies | Dev deps |
peerDependencies | Host requirements |
engines | Node/npm versions |
private | Prevent publish |
bin | CLI executables |
files | Published files |
workspaces | Monorepo packages |
Semver Ranges
| Range | Allows |
|---|---|
1.2.3 | Exact |
^1.2.3 | Minor + patch |
~1.2.3 | Patch only |
>=1.2.3 | Higher |
* | Any |
latest | Latest |
npm Commands
| Command | Purpose |
|---|---|
npm init -y | Create package.json |
npm install | Install from package.json |
npm ci | Install from lockfile |
npm install PKG | Add dependency |
npm install -D PKG | Add dev dependency |
npm uninstall PKG | Remove |
npm update | Update within ranges |
npm outdated | List newer versions |
npm run SCRIPT | Run a script |
npm audit | Security scan |
npm list | Show tree |
npx PKG | Run without install |
yarn Commands
| Command | Purpose |
|---|---|
yarn | Install |
yarn add PKG | Add |
yarn add -D PKG | Add dev |
yarn remove PKG | Remove |
yarn run SCRIPT | Run script |
yarn install --frozen-lockfile | CI install |
pnpm Commands
| Command | Purpose |
|---|---|
pnpm install | Install |
pnpm add PKG | Add |
pnpm add -D PKG | Add dev |
pnpm remove PKG | Remove |
pnpm run SCRIPT | Run script |
pnpm install --frozen-lockfile | CI install |
Lockfiles
| Manager | File |
|---|---|
| npm | package-lock.json |
| yarn | yarn.lock |
| pnpm | pnpm-lock.yaml |
Manager Comparison
| Aspect | npm | yarn | pnpm |
|---|---|---|---|
| Speed | Good | Good | Fastest |
| Disk | High | High | Low |
| Phantom deps | Yes | Yes | No |
| Monorepo | OK | Good | Best |
| Ships with Node | ✅ | ❌ | ❌ |
Dependency Types
| Type | Installed | Published |
|---|---|---|
dependencies | Always | Consumers |
devDependencies | Dev only | Never |
peerDependencies | Consumer | Declared |
optionalDependencies | If possible | Consumers |
Install vs ci
| Aspect | install | ci |
|---|---|---|
| Reads | package.json | lockfile |
| Writes lockfile | ✅ | ❌ |
| Fails on mismatch | ❌ | ✅ |
| Speed | Slower | Faster |
| Use | Development | CI/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
| Pitfall | Problem | Solution |
|---|---|---|
Committing node_modules | Huge repo | .gitignore |
| No lockfile in git | Version drift | Commit lockfile |
| Mixing managers | Two lockfiles | Pick one |
npm install in CI | Non-deterministic | npm ci |
| Phantom deps | Breaks on updates | pnpm or audit |
No engines | Wrong Node | Set + enforce |
* versions | Anything installs | Pin ranges |
| Global installs | Version drift | Prefer local |
| Publishing secrets | Leak | "private": true |
| Upgrading all at once | Widespread breakage | One 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
| Concept | Field / Tool | Purpose |
|---|---|---|
| Manifest | package.json | Declares project |
| Scripts | scripts | Named commands |
| Runtime deps | dependencies | Needed always |
| Dev deps | devDependencies | Needed in dev |
| Peer deps | peerDependencies | Host-provided |
| Entry | main / module / exports | Import resolution |
| ESM | type | Module system |
| Version pin | engines | Node/npm versions |
| Private | private | Prevent publish |
| Lockfile | package-lock.json | Exact versions |
| Install | npm install | From manifest |
| Clean install | npm ci | From lockfile |
| Run | npm run | Execute script |
| Add | npm install | Add dep |
| Remove | npm uninstall | Remove dep |
| Check | npm outdated | Newer versions |
| Audit | npm audit | Security |
| One-off | npx | Temporary run |
| Manager | npm / yarn / pnpm | Choose one |
Key takeaways:
package.jsonis the manifest — name, version, scripts, dependenciesscriptsdefine repeatable commands — run withnpm rundependenciesare for runtime;devDependenciesfor development- Use semver ranges —
^1.2.3for compatible upgrades, no* type: "module"enables ESM; omit for CommonJSprivate: trueprevents accidental publishingenginespins the Node version — enforce it in CI- Lockfiles record exact versions — always commit them
npm ciis for CI — fast, deterministic, fails on mismatchnpm installis for development — may update the lockfilenpxruns 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=devfor 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!