DEV Community

Alex Chen
Alex Chen

Posted on

npm Scripts and package.json Mastery (2026)

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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"]
  }
}
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

What's your favorite npm trick? What package.json habit do you swear by?

Follow @armorbreak for more practical developer guides.

Top comments (0)