Dev Encyclopedia
ArticlesTools

Get notified when new content drops

No spam. Just new articles, tools, and updates — straight to your inbox.

Dev Encyclopedia

A reference for builders

Content

  • Articles
  • Tools
  • Contact

Connect

  • support@devencyclopedia.com
  • RSS Feed

© 2026 Dev Encyclopedia

Privacy PolicyTermsDisclaimer
  1. Home
  2. /Blog
  3. /npm Scripts You're Probably Not Using (But Should Be)
javascript8 min read

npm Scripts You're Probably Not Using (But Should Be)

pre/post hooks, cross-env, npm-run-all, argument passing, and built-in variables — the npm script patterns developers Google one at a time, in one place.

By Dev EncyclopediaPublished June 1, 2026
On this page

On this page

  • pre and post Lifecycle Hooks
  • Passing Arguments with --
  • Referencing Other Scripts
  • Cross-Platform Env Vars with cross-env
  • Parallel and Serial with npm-run-all
  • Node's Built-in --watch Flag
  • The $npm_package_* Variables
  • A Naming Convention That Scales
  • Frequently Asked Questions

Every JavaScript project has a package.json with a handful of scripts. Most stop at dev, build, and start. But npm scripts have a surprising amount of built-in power — lifecycle hooks, argument forwarding, cross-platform environment variables, parallel execution, and built-in package variables — that eliminates the need for separate tooling in many cases.

These eight patterns appear constantly in mature JavaScript projects but almost never make it into beginner tutorials. None of them require much setup — once you've seen them, you'll reach for them immediately.

  1. 1

    The pre and post Lifecycle Hooks

    Prefix any script name with pre or post and npm runs it automatically before or after the main script — no configuration required. If the pre script exits with a non-zero code, the main script never runs.

    json
    {
      "scripts": {
        "prebuild": "npm run typecheck",
        "build": "next build",
        "postbuild": "node scripts/generate-sitemap.js"
      }
    }
    • prebuild — runs automatically before npm run build; a failing typecheck stops the build entirely
    • postbuild — runs automatically after a successful build; sitemap generation, asset upload, or any cleanup step
    • preinstall — useful for checking the Node.js or npm version before dependencies install
    • prepare — runs after npm install and before npm publish; this is where Husky registers git hooks

    💡 Tip

    You don't need to update a CI config every time you add a pre/post step — npm run build in your pipeline automatically picks them up. The hooks live in the same place as the script they guard.

  2. 2

    Passing Arguments with --

    Everything after -- in an npm run command is forwarded to the underlying script as arguments. This lets you define a base command in package.json and extend it at the command line without creating a separate script entry for every variation.

    bash
    # npm run <script> -- <args forwarded to the command>
    npm run test -- --watch
    npm run lint -- --fix
    npm run build -- --profile
    
    # Equivalent to running directly:
    # jest --watch
    # eslint src/ --fix
    # next build --profile
    • Only one -- separator is needed regardless of how many arguments follow it
    • Arguments are appended to the end of the script command — they cannot be inserted in the middle
    • Works with any CLI: jest, eslint, tsc, next, and others

    ℹ Info

    Define the minimal base command in package.json. Use -- at the command line for one-off variations. You rarely need separate test:watch and test:coverage entries — npm run test -- --watch covers both.

  3. 3

    Referencing Other Scripts Inside Scripts

    Scripts can call other scripts using npm run <name>. This lets you build reusable primitives and compose them into higher-level commands. The && operator runs the second command only if the first exits successfully — use it to enforce quality gates.

    json
    {
      "scripts": {
        "typecheck": "tsc --noEmit",
        "lint": "eslint src/",
        "format": "prettier --write src/",
        "check": "npm run typecheck && npm run lint",
        "fix": "npm run format && npm run lint -- --fix"
      }
    }
    • npm run check runs both type check and lint — lint only runs if typecheck passes
    • npm run fix formats then fixes lint errors — composing two primitives into one convenient command
    • Prefer npm run name over duplicating the underlying command — if the tool changes, you update one place

    ℹ Info

    Use && for two-command gates. Use run-s from npm-run-all (Pattern 5) when you need to chain more than two or three scripts — it is more readable and handles failures cleanly.

    ⚠ Cross-platform gotchas

    KEY=value command is POSIX-only (macOS/Linux). & for parallel is unsupported on Windows. %VARIABLE% vs $VARIABLE syntax differs by shell. Use cross-env (Pattern 4) and run-p (Pattern 5) for portable scripts.

  4. 4

    Cross-Platform Environment Variables with cross-env

    Setting environment variables inline with KEY=value command is POSIX shell syntax — it works on macOS and Linux but breaks on Windows Command Prompt and PowerShell. cross-env solves this with zero configuration.

    json
    {
      "scripts": {
        "dev": "cross-env NODE_ENV=development next dev",
        "build:staging": "cross-env NODE_ENV=staging ANALYZE=true next build",
        "build:prod": "cross-env NODE_ENV=production next build"
      }
    }
    • Install once: npm install --save-dev cross-env
    • Identical syntax on macOS, Linux, and Windows — the package handles the platform translation
    • Supports multiple variables in one call: cross-env NODE_ENV=staging PORT=3001 next start

    💡 Tip

    If your project is open-source or has contributors on Windows, cross-env is a two-minute fix that prevents a class of 'works on my machine' issues. Check your package.json scripts for any KEY=value prefix — each one is a potential Windows breakage.

  5. 5

    Parallel and Serial Execution with npm-run-all

    npm runs scripts sequentially by default. npm-run-all adds run-p (parallel) and run-s (serial) commands for when you need more control. run-s stops the chain immediately if any script fails — ideal for build pipelines.

    json
    {
      "scripts": {
        "dev": "run-p dev:app dev:worker",
        "dev:app": "next dev",
        "dev:worker": "node --watch src/worker.js",
    
        "build": "run-s build:check build:app",
        "build:check": "npm run typecheck && npm run lint",
        "build:app": "next build"
      }
    }

    How run-p and run-s compare to the alternatives:

    SequentialParallel
    npm defaultYes (scripts run one at a time)No
    &&Yes (stops on failure)No
    & (ampersand)NoYes — broken on Windows
    run-sYes (stops on failure)No
    run-pNoYes — cross-platform
    • Install: npm install --save-dev npm-run-all
    • run-p starts all matched scripts simultaneously — wall-clock time equals the slowest one
    • run-s runs scripts one at a time and stops on the first failure
    • Glob patterns work: run-p watch:* runs every watch: script without listing each one

    ⚠ Warning

    Avoid using & (single ampersand) for parallel execution — it is not supported on Windows. Use run-p instead.

  6. 6

    Node's Built-in --watch Flag

    Since Node.js 18, you can run any file with --watch and it automatically restarts when the file or its imported dependencies change. No nodemon required for background scripts.

    json
    {
      "scripts": {
        "dev": "run-p dev:app dev:worker",
        "dev:app": "next dev",
        "dev:worker": "node --watch src/worker.js",
        "dev:codegen": "node --watch scripts/generate-types.js",
        "dev:mailer": "node --watch src/queue/mailer.js"
      }
    }
    • Restarts on file changes to the entry file and any require/import dependencies it loads
    • Zero configuration and zero additional dependencies — it is part of the Node.js runtime
    • Pair with run-p to run your main app server alongside background workers that auto-restart

    ℹ Info

    Use node --watch for background scripts: data generation, file watchers, queue workers. For Next.js itself, keep using next dev which has its own reloading mechanism.

  7. 7

    The $npm_package_* Built-in Variables

    npm exposes every field in your package.json as an environment variable during script execution. The most immediately useful is $npm_package_version.

    json
    {
      "version": "2.1.4",
      "scripts": {
        "build": "next build",
        "tag": "git tag v$npm_package_version && git push --tags"
      }
    }
    • npm run tag creates v2.1.4 automatically — no hardcoding the version string in two places
    • $npm_package_name gives you the package name, $npm_package_description the description
    • Nested fields use double underscores: $npm_package_repository__url for repository.url

    ⚠ Warning

    On Windows, the syntax is %npm_package_version% — not $npm_package_version. For cross-platform scripts, use a Node.js inline: node -e "console.log(process.env.npm_package_version)" or cross-env.

  8. 8

    A Script Naming Convention That Scales

    As projects grow, a flat list of scripts becomes hard to navigate. Using : as a namespace separator turns your scripts section into self-documenting project structure.

    json
    {
      "scripts": {
        "dev": "run-p dev:*",
        "dev:app": "next dev",
        "dev:worker": "node --watch src/worker.js",
    
        "build": "run-s build:check build:app",
        "build:check": "npm run typecheck && npm run lint",
        "build:app": "next build",
    
        "db:generate": "drizzle-kit generate",
        "db:migrate": "drizzle-kit migrate",
        "db:push": "drizzle-kit push",
        "db:studio": "drizzle-kit studio"
      }
    }
    • dev:*, build:*, db:* — namespaced scripts are immediately readable without documentation
    • Top-level dev and build orchestrate the namespaced ones — new developers run the simple command, power users run the specific one
    • run-p dev:* automatically picks up any new dev:* script you add — no need to update the parent
    • Consistent across projects: a developer who has seen this convention once can navigate any project using it

Frequently Asked Questions

What is the difference between run-p and run-s in npm-run-all?
  • `run-p` (parallel) — starts all matched scripts simultaneously. Use for dev mode: run-p dev:app dev:worker.
  • `run-s` (serial) — runs scripts one at a time in order, stopping if any script fails. Use for builds: run-s typecheck lint build.
  • Glob support: both accept wildcard patterns. run-p watch:* starts every watch: script. run-s build:* runs them in order.
Do pre and post hooks run with npm ci?

npm ci triggers the same lifecycle hooks as npm install. preinstall, postinstall, and prepare all run. This is why Husky recommends wrapping the prepare script: "prepare": "husky || true" — so it doesn't fail in environments where .git is absent.

Why does NODE_ENV=development break on Windows?

KEY=value command is POSIX shell syntax. Windows Command Prompt and PowerShell don't support it.

bash
# ❌ Breaks on Windows
NODE_ENV=development next dev

# ✅ Cross-platform with cross-env
cross-env NODE_ENV=development next dev

Install once: npm install --save-dev cross-env. Syntax is identical on all platforms after that.

What is the difference between && and & in npm scripts?
&&&
BehaviorSequential — second runs only if first succeedsBackground — runs second without waiting
Cross-platform?YesNo — broken on Windows
Use forQuality gates: typecheck && buildUse run-p instead
When should I use a shell script instead of an npm script?

Use an npm script for running CLI commands, composing scripts, or injecting environment variables. Reach for a Node.js script when you need if/else branches, file loops, or long logic that needs comments:

json
{
  "scripts": {
    "deploy": "node scripts/deploy.js"
  }
}
javascript — scripts/deploy.js
// Readable, testable, accepts arguments
const env = process.argv[2] || 'staging';
console.log(`Deploying to ${env}...`);
// conditional logic here

The naming convention pattern is worth pausing on. dev:*, build:*, db:* turns a flat list of scripts into self-documenting project structure — a new team member can read it and understand what the project does without asking.

Most of these patterns replace tools you might reach for separately: Makefiles for task composition, nodemon for file watching, dotenv CLI for environment injection. Check how much of that tooling your package.json scripts can handle before adding another dependency.

Related Articles

javascript

5 async/await Mistakes That Slow Your JavaScript Code

Sequential awaits, await in forEach, missing Promise.all — these 5 async/await mistakes silently slow your JavaScript. Here's how to spot and fix each one.

May 30, 2026·8 min read

On this page

  • pre and post Lifecycle Hooks
  • Passing Arguments with --
  • Referencing Other Scripts
  • Cross-Platform Env Vars with cross-env
  • Parallel and Serial with npm-run-all
  • Node's Built-in --watch Flag
  • The $npm_package_* Variables
  • A Naming Convention That Scales
  • Frequently Asked Questions