npm Scripts and package.json: The Complete Guide (2026)
Most developers only use npm start and npm install. Here's everything else you're missing.
Understanding package.json
{
"name": "my-awesome-project",
"version": "1.2.3", // Semantic versioning (MAJOR.MINOR.PATCH)
"description": "A brief description",
"type": "module", // ESM! Use "module" for import/export (Node 18+ default)
// ⚠️ NEVER commit these files:
"private": true, // Prevents accidental `npm publish`
"bin": {
"mytool": "./cli.js" // CLI command name → entry point
},
"main": "./dist/index.js", // CommonJS entry point (require())
"exports": { // Modern entry points (ESM)
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
},
"files": [ // What gets published to npm
"dist",
"README.md"
],
"engines": {
"node": ">=18.0.0" // Minimum Node.js version
},
"scripts": { /* ... */ }, // We'll cover this in detail
"dependencies": { // Production dependencies
"express": "^4.21.0",
"lodash": "~4.17.21"
},
"devDependencies": { // Development-only dependencies
"typescript": "^5.6.0",
"jest": "^29.7.0",
"eslint": "^9.14.0"
},
"peerDependencies": { // Required but not installed by this package
"react": ">=18.0.0"
},
"optionalDependencies": { // Won't fail install if missing
"fsevents": "^2.3.3" // macOS-specific, for example
},
"browserslist": [ # For autoprefixer/postcss/babel
"> 0.5%",
"not dead",
"not op_mini all"
]
}
Version Ranges Explained
{
"express": "4.21.0", // Exact version — always use exactly 4.21.0
"express": "~4.21.0", // ~ = Patch-level updates: >=4.21.0 <4.22.0
"express": "^4.21.0", // ^ = Minor-level: >=4.21.0 <5.0.0 (DEFAULT!)
"express": ">=4.18.0", // Any version 4.18.0 or higher
"express": ">=4.18.0 <5.0.0", // Range with upper bound
"express": "latest", // Always latest (risky for production!)
"express": "github:user/repo", // Install from GitHub directly
"express": "link:../local-pkg",// Link to local package (development)
"express": "workspace:*", // npm workspace (monorepo)
"express": "" // Empty = any version (wildcard, avoid!)
}
My recommendation for most projects:
- Dependencies:
^(caret) — get patches and minor features - DevDependencies: exact versions or
^depending on team preference - Lock file (
package-lock.json) pins exact versions regardless
Scripts: The Power You're Not Using
Built-in Script Variables
{
"scripts": {
// These variables are available in ALL scripts:
// $npm_package_name → "my-awesome-project"
// $npm_package_version → "1.2.3"
"echo:name": "echo Package name is: $npm_package_name",
// These are available when running via 'npm run':
// npm_config_* → Any npm config value as env var
// npm_lifecycle_event → Current script name ("start", "build", etc.)
"what-am-i": "echo Running: $npm_lifecycle_event"
}
}
Lifecycle Scripts (Automatic)
{
"scripts": {
// These run AUTOMATICALLY at specific times:
"preinstall": "echo 'About to install...'",
"install": "node scripts/post-install.js",
"postinstall": "npm run build || true", // Runs after every npm install!
"prepublishOnly": "npm run test && npm run build", // Before npm publish ONLY
// Git hooks (via husky or similar):
// "precommit": "lint-staged",
// "prepush": "npm test"
}
}
Practical Script Patterns
{
"scripts": {
// === Development ===
"dev": "node --watch server.js",
"dev:debug": "node --inspect --watch server.js",
// === Building ===
"build": "tsc && esbuild src/index.ts --bundle --outfile=dist/index.js --platform=node",
"build:watch": "tsc --watch",
"clean": "rm -rf dist node_modules/.cache",
// === Testing ===
"test": "node --test",
"test:watch": "node --test --watch",
"test:coverage": "node --experimental-test-coverage --test",
"test:e2e": "playwright test",
// === Linting & Formatting ===
"lint": "eslint src/ --max-warnings 0",
"lint:fix": "eslint src/ --fix",
"format": "prettier --write 'src/**/*.{js,ts,json}'",
"format:check": "prettier --check 'src/**/*.{js,ts,json}'",
// Type checking (separate from build!)
"typecheck": "tsc --noEmit",
// Combined quality check (run before commits/pushes)
"quality": "npm run lint && npm run typecheck && npm run test",
"quality:fix": "npm run lint:fix && npm run format && npm run typecheck",
// === Database ===
"db:migrate": "node scripts/migrate.js",
"db:seed": "node scripts/seed.js",
"db:reset": "npm run db:migrate -- --force && npm run db:seed",
"db:studio": "prisma studio", // Visual DB browser
// === Docker ===
"docker:build": "docker build -t myapp .",
"docker:up": "docker compose up -d",
"docker:down": "docker compose down",
"docker:logs": "docker compose logs -f",
// === Deployment ===
"deploy:staging": "npm run build && rsync -avz dist/ staging:/app/",
"deploy:prod": "npm run build && rsync -avz dist/ prod:/app/",
// === Utility ===
"prepare": "husky", // Husky git hooks setup
"release": "standard-version", // Auto-changelog + version bump
"update:deps": "npx npm-check-updates -u && npm install", // Update all deps
"outdated": "npm outdated", // Check for outdated packages
"audit": "npm audit", // Security audit
"audit:fix": "npm audit fix", // Fix vulnerabilities
"clean:cache": "rm -rf node_modules/.cache .eslintcache",
// === Monorepo (if using workspaces) ===
"workspaces:list": "npm ws ls",
"workspaces:run": "npm ws exec -- npm run build",
"workspaces:test": "npm ws exec -- npm run test"
}
}
Running Scripts: Tips & Tricks
# Basic usage
npm run dev # Run the "dev" script
npm test # Shorthand for "test" (no "run" needed for test/start/stop)
# Pass arguments
npm run dev -- --port 8080 # Everything after "--" goes to the script
npm test -- --grep "auth" # Pass args to test runner
# Run multiple scripts sequentially
npm run lint && npm run test
# Run scripts in parallel (Unix)
npm run lint & npm run typecheck & wait
# Pre/Post hooks run automatically:
npm run build
# Actually runs: prebuild → build → postbuild (if defined)
# Environment variables (cross-platform!)
npm_config_production=1 npm run build
# Or use cross-env: npx cross-env NODE_ENV=production npm run build
# List all available scripts
npm run
# See what a script would do without running it
npm run build --dry-run # (if using certain tools)
# Run a script from a dependency's package.json
npx jest # Runs jest from node_modules
npx tsc --init # Runs typescript's init
Managing Dependencies
# Install (adds to package.json automatically)
npm install express # Save to dependencies
npm install -D typescript # Save to devDependencies (-D = --save-dev)
npm install -P husky # Save to optionalDependencies (-P = --save-optional)
# Install specific version
npm install express@4.18.0
npm install express@">=4.18 <5"
# Install from GitHub
npm install github:user/repo
npm install github:user/repo#v1.0.0 # Specific tag/branch/commit
# Global installs (CLI tools)
npm install -g nodemon pnpm
# But prefer npx for one-off usage!
# Remove
npm uninstall lodash
npm uninstall -D jest
# Update
npm update # Updates all deps per version ranges
npm update express # Updates only express
npm install express@latest # Force latest (updates range too!)
# Check status
npm outdated # Shows outdated packages
npm ls express # Shows why express was installed (dependency tree)
npm ls express --depth=0 # Top level only
# Auditing
npm audit # Check for security vulnerabilities
npm audit fix # Auto-fix if possible
npm audit fix --force # Force fix (may break things, review changes!)
# Clean install (like fresh clone)
rm -rf node_modules package-lock.json && npm install
# Dedupe (flatten dependency tree)
npm dedupe # Merges duplicate dependencies where possible
package-lock.json: Why It Matters
package-lock.json locks EXACT versions of EVERY dependency.
Including transitive dependencies.
Why it matters:
→ Teammates install identical versions (reproducible builds)
→ CI/CD installs identical versions
→ Prevents "works on my machine" bugs
→ npm ci uses it for fast, exact installs
RULES:
1. Commit it to version control ✅
2. Don't edit it manually ❌
3. Use npm ci (not npm install) in CI environments
4. If you hit weird bugs: rm package-lock.json && npm install (last resort)
npm vs pnpm vs yarn (2026)
| Feature | npm | pnpm | yarn (v1/v4) |
|---|---|---|---|
| Speed | Fast | Fastest (hard links) | Fast |
| Disk usage | Duplicates in nested node_modules | Efficient (content-addressable) | Moderate |
| Strict mode |
--strict flag |
Default strict | Optional |
| Workspaces | ✅ npm workspaces | ✅ | ✅ |
| Lockfile | package-lock.json | pnpm-lock.yaml | yarn.lock |
| CI install | npm ci |
pnpm install --frozen-lockfile |
yarn install --frozen-lockfile |
| Popularity | Default (ships with Node) | Growing fast | Stable |
My take: npm is perfectly fine for most projects. pnpm wins for monorepos with many packages.
Common package.json Mistakes
// ❌ Missing "private": true
// Risk: Someone could accidentally npm publish your code!
// ❌ "type": missing (defaults to CommonJS)
// In 2026, you almost certainly want "type": "module"
// ❌ Files not in .gitignore but shouldn't be published
// Fix: Add "files" array or .npmignore
// ❌ Scripts that don't work cross-platform
{ "clean": "rm -rf dist" } // Fails on Windows!
// Fix: Use rimraf or shelljs: { "clean": "rimraf dist" }
// ❌ Not pinning engines
// Fix: Always add "engines": { "node": ">=18" }
// ❌ Huge dependencies for tiny tasks
// Don't add lodash just for _.map() — use Array.prototype.map()
// Don't add moment.js — use date-fns or native Intl API
// ❌ Outdated dependencies causing security issues
// Fix: Run `npm audit` regularly, set up Dependabot/GitHub Actions
What's your favorite npm script trick?
Follow @armorbreak for more practical developer guides.
Top comments (0)