NodeJSnpm Scripts

npm Scripts

The scripts field turns package.json into your project's task runner. Instead of memorizing long command lines, you give them names — npm run dev, npm test, npm run build — and everyone on the team uses the same vocabulary. Scripts also chain, hook into lifecycle events, and run locally-installed tools without global installs.

Defining and running

package.json

JSON
{
  "scripts": {
    "dev": "node --watch src/index.js",
    "build": "tsc -p .",
    "test": "node --test",
    "lint": "eslint .",
    "start": "node dist/index.js"
  }
}

Bash
npm run dev        # run any script by name
npm run            # list all available scripts
npm test           # special: no "run" needed
npm start          # special: no "run" needed
Four scripts skip the `run` keyword
`start`, `test`, `stop`, and `restart` are "lifecycle" script names — npm lets you call them directly (`npm test`, `npm start`). Every *other* name needs `npm run <name>`. `npm run` with no argument prints the full list, which is the fastest way to discover a project's commands.
The local .bin secret

Why can you write "build": "tsc" when TypeScript was installed locally, not globally? Because npm prepends node_modules/.bin to PATH for the duration of the script. Every package with a bin entry drops an executable there:

What npm adds to PATH during a script

Text
node_modules/.bin/
   ├─ tsc        →  symlink into node_modules/typescript
   ├─ eslint     →  symlink into node_modules/eslint
   └─ jest       →  symlink into node_modules/jest

So "tsc" in a script resolves here FIRST — no global install needed.
Bare tool names only work inside scripts
Typing `tsc` directly in your terminal fails unless TypeScript is installed globally — your shell does not know about `node_modules/.bin`. Inside an npm script it works because npm augments `PATH`. To run a local tool ad-hoc from the terminal, use `npx tsc` (see [npx](/nodejs/npx)).
Pre and post hooks

For any script x, npm automatically runs prex before it and postx after — if those scripts exist. This is implicit; you never call them directly:

JSON
{
  "scripts": {
    "prebuild": "rimraf dist",
    "build": "tsc -p .",
    "postbuild": "echo Build complete"
  }
}
$ npm run build
> prebuild   (rimraf dist)
> build      (tsc -p .)
> postbuild  (echo Build complete)
Some lifecycle hooks fire on install too
Beyond `pre`/`post`, npm runs certain scripts at well-known moments: `prepare` runs after `npm install` and before `npm publish` (great for building a library); `postinstall` runs after dependencies install. Be cautious — `postinstall` scripts in *dependencies* execute arbitrary code on your machine, a known supply-chain risk (see [Dependency & Secret Security](/nodejs/dependency-security)).
Chaining and composing

Combine scripts with shell operators or by calling other scripts. && runs sequentially (stop on failure); & runs in parallel:

JSON
{
  "scripts": {
    "lint": "eslint .",
    "typecheck": "tsc --noEmit",
    "test": "node --test",
    "check": "npm run lint && npm run typecheck && npm run test",
    "ci": "npm run check"
  }
}
`&&` and `&` are shell features, not npm's — and differ on Windows
Chaining with `&&` relies on the OS shell, and parallel `&` behaves differently (or breaks) on Windows `cmd`. For reliable cross-platform parallelism, use a dedicated tool like `npm-run-all` (`run-s` for serial, `run-p` for parallel) or `concurrently` instead of raw `&`.
Passing arguments

Use -- to forward extra arguments through npm to the underlying command:

Bash
npm test -- --watch          # passes --watch to the test runner
npm run lint -- --fix        # passes --fix to eslint
Reading config and env

Scripts run with a rich environment: every package.json field is exposed as npm_package_*, and npm config as npm_config_*:

JSON
{
  "version": "2.4.1",
  "scripts": { "release": "echo Releasing v$npm_package_version" }
}
$ npm run release
Releasing v2.4.1
Next
Run package binaries without adding them to scripts or installing globally: [Using npx](/nodejs/npx).