npm Scripts and package.json Mastery (2026)
Your package.json is more than a dependency list — it's the command center of your project. Master it and you'll never leave your terminal.
package.json Anatomy
{
"name": "my-awesome-project",
"version": "1.2.3",
// Semantic versioning: MAJOR.MINOR.PATCH
// MAJOR = breaking changes (2.0.0)
// MINOR = new features (1.3.0) — backward compatible
// PATCH = bug fixes (1.2.4) — fully backward compatible
"description": "An awesome project",
"main": "./dist/index.cjs", // CJS entry point
"module": "./dist/index.mjs", // ESM entry point
"types": "./dist/index.d.ts", // TypeScript types
"bin": { // CLI commands (for npm packages)
"mytool": "./cli.js"
},
"files": [ // What gets published to npm
"dist",
"README.md"
],
"type": "module", // Treat .js as ESM!
"scripts": {
// We'll cover this in detail below
},
"dependencies": {
// Production dependencies (installed with --save or -S)
"express": "^4.18.2",
"lodash": "~4.17.21"
},
"devDependencies": {
// Development-only (not installed in production)
"typescript": "^5.0.0",
"jest": "^29.0.0",
"eslint": "^8.50.0"
},
"peerDependencies": {
// Required but NOT auto-installed (host project provides)
"react": ">=18.0.0"
},
"optionalDependencies": {
// Nice to have; install continues if they fail
"fsevents": "^2.3.0" // macOS-only, fails silently on Linux
},
"engines": {
"node": ">=18.0.0",
"npm": ">=9.0.0"
},
"browserslist": [ // For autoprefixer/babel
"> 1%",
"last 2 versions",
"not dead"
],
"config": { // Custom config for scripts
"port": "3000",
"env": "development"
}
}
Version Ranges Explained
Version range syntax (the ^ ~ > < = characters):
^1.2.3 → >=1.2.3 <2.0.0 (Caret: left-most non-zero can change)
~1.2.3 → >=1.2.3 <1.3.0 (Tilde: patch only)
>1.2.3 → >1.2.3 (Greater than)
>=1.2.3 → >=1.2.3 (Greater or equal)
<2.0.0 → <2.0.0 (Less than)
* / x → Any version (Latest always)
1.2.x → >=1.2.0 <1.3.0 (Wildcard)
1.2.3 - 2.0.0 → Range between versions
file:../local-package → Local path
git+https://repo.git → Git repository
npm:@scope/package@latest → Scoped package
Best practice for apps:
"express": "^4.18.2" // Get patches + minor updates automatically
Best practice for libraries:
"express": "~4.18.2" // Only get patches (safer for published code)
Lock file (package-lock.json):
Pins EXACT versions of every dependency (including transitive!)
Commit this file! It ensures reproducible installs.
npm ci (not npm install!) uses lock file exactly.
Powerful npm Scripts
{
"scripts": {
// === Basic ===
"start": "node server.js", // npm start (special command!)
"test": "jest", // npm test (special!)
// === Development ===
"dev": "nodemon server.js", // Auto-restart on changes
"dev:debug": "node --inspect server.js", // Debug mode
// === Build Pipeline ===
"build": "tsc && vite build",
"build:watch": "tsc --watch & vite build --watch",
"build:clean": "rm -rf dist && npm run build",
// === Linting & Formatting ===
"lint": "eslint 'src/**/*.{js,ts}'",
"lint:fix": "eslint 'src/**/*.{js,ts}' --fix",
"format": "prettier --write 'src/**/*.{js,ts,json,md}'",
"check-types": "tsc --noEmit",
// === Combined Checks (CI-friendly) ===
"check": "npm run lint && npm run check-types && npm run test",
"check:ci": "npm run lint -- --max-warnings=0 && npm run check-types && npm run test -- --coverage",
// === Database ===
"db:migrate": "prisma migrate dev",
"db:seed": "prisma db seed",
"db:reset": "prisma migrate reset --force",
"db:studio": "prisma studio",
// === Deployment ===
"deploy": "npm run build && rsync -avz dist/ user@server:/app/",
"deploy:staging": "NODE_ENV=staging npm run deploy",
"deploy:prod": "NODE_ENV=production npm run deploy",
// === Git Hooks (via husky + lint-staged) ===
"prepare": "husky install",
"pre-commit": "lint-staged",
"commitmsg": "commitlint -E HUSKY_GIT_PARAMS"
},
"lint-staged": {
"*.{js,ts}": ["eslint --fix", "prettier --write"],
"*.{json,md}": ["prettier --write"]
}
}
Script Tips & Tricks
# === Running Scripts ===
npm run dev # Run a script
npm start # Special: no "run" needed for "start"/"test"
npm run build -- --watch # Pass arguments after --
# === Pre/Post Hooks (automatic!) ===
# If you define these:
# "prebuild": echo 'Building...'"
# "build": tsc
# "postbuild": echo 'Done!'
# Running `npm run build` automatically executes: prebuild → build → postbuild
# Available hooks: pre/post + script name
# preinstall / postinstall (on npm install)
# prepublishOnly / postpublish (before publishing)
# preversion / postversion (on npm version)
# prestart / poststart (on npm start)
# pretest / posttest (on npm test)
# Example: Auto-generate client before starting:
# "prestart": "npm run generate-api-client",
# "start": "node server.js"
# === Environment Variables in Scripts ===
# Cross-platform env vars (works on Windows/Mac/Linux):
cross-env NODE_ENV=production node server.js
# Or use .env files:
# dotenv CLI: dotenv -- node server.js
# === Sequential vs Parallel Execution ===
# Sequential (&&): runs left-to-right, stops on failure
"full-check": "npm run lint && npm run type-check && npm run test"
# Parallel (&): runs simultaneously
"validate:parallel": "npm run lint & npm run type-check & npm run test"
# For complex parallel: use npm-run-all
"check:all": "npm-run-all --parallel lint type-check test"
# === Using Config from package.json ===
# In your scripts, access custom config:
# process.env.npm_package_config_port → "3000"
# process.env.npm_package_version → "1.2.3"
# === Lifecycle Scripts (important for packages!) ===
// package.json for a publishable library:
{
"scripts": {
"prepublishOnly": "npm run build && npm run test", // Before npm publish
"prepack": "npm run clean", // Before creating tarball
"postinstall": "node setup.js" // After users install your package
}
}
// postinstall is great for setup steps (e.g., generating binaries, downloading assets)
// But don't abuse it — users hate surprise side effects!
Managing Dependencies Like a Pro
# === Installing ===
npm install express # Add to dependencies (-S implied)
npm install -D jest # Add to devDependencies (--save-dev)
npm install -P typescript # Add as peer dependency
npm install -O fsevents # Add as optional dependency
# === Updating ===
npm update # Update all deps (per version ranges)
npm update express # Update one package
npx npm-check-updates -u # Check for newer versions, update package.json
# === Auditing ===
npm audit # Check for vulnerabilities
npm audit fix # Auto-fix where possible
npm audit fix --force # Force fix (may break things!)
# === Removing ===
npm uninstall lodash # Remove from package.json + node_modules
# === Inspecting ===
npm list # Show installed deps (tree view)
npm list --depth=0 # Only top-level deps
npm list express # Info about specific dep
npm why lodash # Why is this installed? (who depends on it?)
npm ls @types/node # Check type definitions
# === Cleaning Up ===
npm prune # Remove extraneous packages
rm -rf node_modules package-lock.json && npm install # Fresh install
# === Global Packages ===
npm list -g --depth=0 # List global packages
npm install -g nodemon # Install globally
npm uninstall -g nodemon # Remove global
# === npx: Run Without Installing ===
npx create-react-app my-app # Run once, don't keep installed
npx typescript@latest --init # Use specific version
npx serve ./dist # Quick static server
npx http-server ./dist -p 8080 # Another static server option
npx @angular/cli new my-app # CLI tools without global install
# === Workspaces (Monorepo Support) ===
# Root package.json:
{
"name": "monorepo",
"private": true,
"workspaces": ["packages/*"],
"scripts": {
"build": "npm run build --workspaces-if-present",
"test": "npm run test --workspaces-if-present",
"clean": "npm exec --workspaces rm -rf dist node_modules"
}
}
# Now you can:
cd packages/shared && npm run build # Builds shared lib
cd packages/app && npm run build # App uses shared from workspace!
# One node_modules at root, shared across all packages.
What's your favorite npm trick? What package.json habit do you swear by?
Follow @armorbreak for more practical developer guides.
Top comments (0)