npm Scripts and package.json Mastery (2026)
Your package.json is more than a dependency list — it's your project's command center. Master npm scripts and supercharge your workflow.
package.json Anatomy
{
"name": "my-awesome-project",
"version": "1.2.3", // Semantic versioning: MAJOR.MINOR.PATCH
"description": "A really cool project",
"main":dist/index.js", // Entry point for require()
"type": "module", // ES Modules mode (import/export)
"bin": { // CLI commands (for npm publish)
"mytool": "./bin/cli.js"
},
"files": [ // Files to include when publishing
"dist",
"README.md",
"LICENSE"
],
"scripts": {
// Your command center → see below
},
"dependencies": {
// Runtime dependencies (installed in production)
},
"devDependencies": {
// Development-only dependencies (not installed in production)
},
"peerDependencies": {
// Required but not auto-installed (host project provides)
},
"optionalDependencies": {
// Nice to have, won't fail install if missing
},
"engines": {
"node": ">=18.0.0", // Minimum Node.js version
"npm": ">=9.0.0"
},
"config": { // Custom config for scripts
"port": 3000,
"env": "development"
}
}
Essential Scripts Every Project Needs
{
"scripts": {
"start": "node dist/index.js",
"dev": "nodemon --watch src src/index.js",
"build": "tsc && esbuild --bundle --minify",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint 'src/**/*.{js,ts}'",
"lint:fix": "eslint 'src/**/*.{js,ts}' --fix",
"format": "prettier --write 'src/**/*.{js,ts,json,md}'",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist node_modules/.cache",
"prepare": "husky install",
"prepublishOnly": "npm run build && npm test"
}
}
Script Composition Patterns
{
"scripts": {
"build": "npm run clean && npm run build:compile && npm run build:copy",
"build:compile": "tsc",
"build:copy": "cp -r src/public dist/",
"test": "npm run test:unit && npm run test:integration",
"test:unit": "jest --testPathPattern=unit",
"test:integration": "jest --testPathPattern=integration",
"deploy": "npm run test && npm run lint && npm run build && npm run deploy:push",
"deploy:push": "git push origin main && ssh server './deploy.sh'"
}
}
// Use colon-separated names for sub-tasks
// Chain with && (stops on failure) or ; (runs all regardless)
// Cross-platform compatible:
// ❌ Windows incompatible: "clean": "rm -rf dist"
// ✅ Cross-platform: "clean": "rimraf dist" (or use shx, del-cli)
Environment Variables in Scripts
# Method 1: Inline (simple, but messy for long values)
"scripts": {
"start": "PORT=3000 NODE_ENV=production node index.js"
}
# Method 2: .env files (use dotenv or env-cmd)
# Install: npm install -D dotenv
"scripts": {
"dev": "dotenv -- node-dev index.js",
"start": "dotenv -- node index.js"
}
# .env file:
# PORT=3000
# DATABASE_URL=postgres://localhost/mydb
# Method 3: cross-env (works on Windows too!)
# Install: npm install -D cross-env
"scripts": {
"build": "cross-env NODE_ENV=production webpack --mode production"
}
# Method 4: npm config variables (built-in since npm v7+)
# In package.json:
"config": {
"port": "3000"
}
# In scripts: use $npm_package_config_port
"scripts": {
"start": "echo Port is $npm_package_config_port && node index.js"
}
# Best practice: Use a .env.example file committed to git:
# PORT=3000
# DATABASE_URL=
# API_KEY=
# JWT_SECRET=
# (Actual values in .env, which is gitignored)
Advanced Script Techniques
{
"scripts": {
# Sequential execution (&& = stop if one fails):
"ci": "npm run lint && npm run typecheck && npm run test && npm run build",
# Parallel execution (& = runs simultaneously):
"parallel": "npm run lint & npm run typecheck & wait",
# Conditional execution:
"precommit": "lint-staged",
# Using npm lifecycle hooks (automatic!):
# npm run X automatically runs preX before and postX after
# Example: "npm run build" runs:
# 1. prebuild script (if defined)
# 2. build script
# 3. postbuild script (if defined)
"prebuild": "echo 'Starting build...'",
"build": "tsc",
"postbuild": "echo 'Build complete!' && npm run size-check",
# Passing arguments to scripts:
# Usage: npm run test -- --grep "auth" --verbose
# The -- separates npm flags from script arguments
# Reading package.json values in scripts:
"info": "echo \"Name: $npm_package_name\" && echo \"Version: $npm_package_version\""
}
}
Dependency Management
# Installing dependencies:
npm install lodash # Save to dependencies (runtime)
npm install -D jest # Save to devDependencies (development only)
npm install -P typescript # Save to peerDependencies
npm install -O optional # Save to optionalDependencies
# Exact versions (reproducible builds):
npm install express@4.18.2 # Exact version
npm install express@^4.18.2 # Compatible (>=4.18.2 <5.0.0) ← DEFAULT
npm install express@~4.18.2 # Patch only (>=4.18.2 <4.19.0)
npm install express@latest # Always latest
# Updating dependencies:
npm outdated # Check what's outdated
npm update # Update to latest within version range
npx npm-check-updates -u # Update package.json to latest versions
npm install # Reinstall based on updated package.json
# Auditing:
npm audit # Check for vulnerabilities
npm audit fix # Auto-fix where possible
npm audit fix --force # Force fix (may break things, use carefully!)
# Removing:
npm uninstall lodash # Remove from dependencies AND node_modules
# Why devDependencies vs dependencies?
# dependencies: Code your project imports at runtime
# Examples: express, lodash, axios, mongoose
# devDependencies: Tools used during development/build/test
# Examples: typescript, eslint, jest, prettier, webpack
# Common mistake: Putting dev tools in dependencies
# This bloats production installs and increases attack surface!
Workspaces (Monorepo Management)
// Root package.json (monorepo setup):
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"packages/*",
"apps/*"
],
"scripts": {
"build": "npm run build --workspaces",
"test": "npm run test --workspaces",
"dev": "npm run dev --workspaces --if-present",
"clean": "npm exec --workspaces -- rm -rf dist node_modules"
}
}
# Structure:
# my-monorepo/
# ├── package.json (root, workspaces defined here)
# ├── packages/
# │ ├── shared-utils/ (shared library)
# │ │ └── package.json
# │ └── ui-components/ (UI component library)
# │ └── package.json
# └── apps/
# ├── web/ (frontend app)
# │ └── package.json
# └── api/ (backend API)
# └── package.json
# Benefits:
# - Single node_modules directory (deduped, saves disk space)
# - Shared dependencies between packages
# - Can reference local packages: "@myorg/shared-utils": "*"
# - Run commands across all packages: npm run test --workspaces
# - Install all at once: npm install (from root)
# Common monorepo tools that extend npm workspaces:
# - Turborepo (remote caching, parallel execution)
# - Nx (smart caching, code generation, affected projects)
# - Lerna (legacy, mostly replaced by built-in workspaces)
What's your favorite npm script trick? How do you organize your package.json?
Follow @armorbreak for more practical developer guides.
Top comments (0)