npm Scripts and package.json Mastery (2026)
Your package.json is more than a dependency list — it's the command center of your project. Here's how to master it.
package.json Deep Dive
{
"name": "my-awesome-project",
"version": "2.1.0",
// Semantic versioning: MAJOR.minor.patch
// MAJOR = breaking changes, minor = new features (backward compatible), patch = bug fixes
"description": "A well-configured Node.js project",
"private": true, // Prevents accidental npm publish!
"type": "module", // Makes .js files use ES Modules
"engines": { // Enforce Node.js version range
"node": ">=18.0.0"
},
"main": "./dist/index.cjs", // Entry point for CommonJS consumers
"module": "./dist/index.mjs", // Entry point for ESM consumers
"types": "./dist/index.d.ts", // TypeScript types entry
"bin": {
"my-tool": "./dist/cli.js" // CLI command name → entry file
},
"exports": { // Explicit exports (prevents internal file access)
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./utils": {
"import": "./dist/utils.mjs",
"require": "./dist/utils.cjs"
}
},
"files": [ // Files included when publishing (whitelist!)
"dist/",
"README.md",
"LICENSE"
},
"scripts": { // The real power of npm! See below...
},
"config": { // Custom config accessible via npm_package_config_*
"port": "3000"
},
"dependencies": { // Production dependencies (installed in prod)
"express": "^4.18.0", // ^ = compatible with version 4.x.x (>=4.18.0 <5.0.0)
"lodash": "~4.17.21" // ~ = patch updates only (>=4.17.21 <4.18.0)
},
"devDependencies": { // Development-only (not installed in production)
"jest": "^29.0.0",
"typescript": "^5.0.0",
"eslint": "^8.0.0"
},
"peerDependencies": { // Required but not auto-installed (host provides)
"react": ">=18.0.0"
},
"optionalDependencies": { // Nice to have; install continues if they fail
"fsevents": "^2.3.0" // macOS-only package
},
"browserslist": [ # Target browsers for autoprefixer/babel
"> 1%", // > 1% global usage
"last 2 versions", // Last 2 versions of each browser
"not dead" // Exclude dead browsers
]
}
Powerful npm Scripts
{
"scripts": {
// Basic scripts
"start": "node dist/server.js",
"dev": "node --watch src/server.js", // Node.js 18+ built-in watch mode!
"build": "tsc && node scripts/build.js",
"test": "jest --coverage",
"lint": 'eslint src/ --ext .ts,.js',
// Pre/post hooks (auto-run before/after script):
"pretest": "npm run lint", // Runs automatically before `npm test`
"postinstall": "node scripts/setup.js", // Runs after `npm install`
"prepublishOnly": "npm run build && npm test", // Runs before `npm publish` only!
// Combined scripts (run multiple in sequence with && or in parallel with &)
"check": "npm run lint && npm run typecheck && npm run test",
"dev:full": "concurrently \"npm run dev\" \"npm run watch:css\"",
// Environment variables in scripts:
"start:prod": "NODE_ENV=production node dist/server.js",
"start:staging": "NODE_ENV=staging PORT=8080 node dist/server.js",
// Cross-platform scripts (no bash-ism!):
"clean": "rimraf dist", // rimraf works on Windows too
"copy-assets": "cpy src/public dist/public",
// Using npm variables:
"info": "echo 'Project: %npm_package_name% v%npm_package_version%'",
"env-info": "env | grep npm_package_", // See all available npm env vars
// Database operations:
"db:migrate": "prisma migrate deploy",
"db:seed": "prisma db seed",
"db:reset": "prisma migrate reset --force",
"db:studio": "prisma studio",
// Deployment:
"deploy:staging": "npm run build && rsync -avz dist/ user@staging:/app/",
"deploy:prod": "npm run build && docker build -t myapp . && docker push myapp:latest"
}
}
# Running scripts:
npm run dev # Run a script
npm start # Shorthand for npm run start (only works for start/test/stop/restart)
npm run build -- --verbose # Pass arguments after --
npx jest # Run without adding to package.json
# Script lifecycle hooks (execution order):
# npm publish:
# prepublishOnly → prepack → pack → postpack → postpublish
# npm install:
# preinstall → install → postinstall
# Useful built-in npm environment variables in scripts:
# $npm_package_name → Project name
# $npm_package_version → Version
# $npm_node_execpath → Path to Node.js binary
# $npm_config_prefix → npm prefix path
# $INIT_CWD → Current working directory when script runs
Dependency Management
# Installing packages:
npm install express # Add to dependencies
npm install -D jest # Add to devDependencies
npm install -P typescript # Explicitly add to dependencies
npm install --save-peer react # Add to peerDependencies
npm install --save-optional fsevents # Add to optionalDependencies
# Version ranges explained:
"express": "4.18.0" # Exact version (pinned)
"express": "~4.18.0" // ~4.18.0 <= v < 4.19.0 (patch updates)
"express": "^4.18.0" # >=4.18.0 <5.0.0 (minor+patch updates)
"express": ">=4.17.0 <5.0.0" # Range expression
"express": "latest" # Always latest (risky for production!)
"express": "github:user/repo" # Install from GitHub directly
# Auditing and updating:
npm outdated # Check for newer versions
npm update # Update per semver ranges in package.json
npm update lodash # Update specific package
npx npm-check-updates -u # Interactive version bump tool
npm audit # Security vulnerability check
npm audit fix # Auto-fix vulnerabilities (when safe)
# Understanding lock files:
# package-lock.json: Exact versions of ALL dependencies (including transitive!)
# Always commit this file! It ensures reproducible installs.
# yarn.lock / pnpm-lock.yaml: Same concept for other package managers
# Removing packages:
npm uninstall lodash # Remove from dependencies + node_modules
npm prune # Remove packages not listed in dependencies
# Why use npx?
npx create-react-app my-app # Run package without installing globally
npx http-server ./dist # One-time use of a CLI tool
npx @typescript-eslint/init # Run specific version of a package
Workspaces (Monorepo Basics)
// Root package.json (monorepo with npm workspaces)
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"packages/*", // Auto-discover all folders under packages/
"apps/*" // Also include apps/
]
}
# Commands from root affect all workspaces:
npm install # Install all workspace deps at once
npm run build # Build all workspaces
npm run test --workspace=packages/shared # Run in specific workspace only
npm ls # Shows dependency graph across workspaces
# Cross-workspace references:
# In packages/web/package.json:
# "dependencies": { "@my-monorepo/shared": "*" } # * = use local workspace version
# npm will symlink it instead of downloading from registry!
What's your favorite npm trick? How do you manage dependencies in large projects?
Follow @armorbreak for more practical developer guides.
Top comments (0)