npm — Task Runners & Scripts
The scripts field in package.jsonis npm's built-in task runner. It allows you to define aliases for CLI commands, chain operations through lifecycle hooks, and orchestrate complex build pipelines — all without external task runners like Gulp or Grunt.
Modern JavaScript projects rely heavily on npm scripts for development, testing, building, and deployment automation. Understanding the lifecycle, argument passing, and cross-platform patterns is essential for writing maintainable automation.
npm automatically runs pre and post lifecycle hooks for every script. If you define prebuild, it runs before build. If you define postbuild, it runs after. This pattern enables clean composition without additional tooling.
| 1 | { |
| 2 | "scripts": { |
| 3 | "prebuild": "rimraf dist && echo 'Cleaning dist...'", |
| 4 | "build": "tsc --project tsconfig.build.json", |
| 5 | "postbuild": "echo 'Build complete!' && cp package.json dist/", |
| 6 | |
| 7 | "pretest": "npm run lint", |
| 8 | "test": "vitest run", |
| 9 | "posttest": "echo 'Tests passed!'", |
| 10 | |
| 11 | "predeploy": "npm run build && npm test", |
| 12 | "deploy": "npx wrangler deploy", |
| 13 | "postdeploy": "npm run healthcheck", |
| 14 | |
| 15 | "healthcheck": "curl -f https://myapp.com/health" |
| 16 | } |
| 17 | } |
| Hook | Runs Before | Typical Use |
|---|---|---|
| prebuild | build executes | Clean, lint, type-check |
| build | — | Main task (build, test, deploy) |
| postbuild | build finishes | Notify, cleanup, health check |
| 1 | # npm build triggers: |
| 2 | # 1. prebuild (if defined) |
| 3 | # 2. build (the script itself) |
| 4 | # 3. postbuild (if defined) |
| 5 | |
| 6 | npm run build |
| 7 | |
| 8 | # Lifecycle output: |
| 9 | # > prebuild |
| 10 | # Cleaning dist... |
| 11 | # > build |
| 12 | # tsc --project tsconfig.build.json |
| 13 | # > postbuild |
| 14 | # Build complete! |
info
npm runs scripts sequentially by default. For parallel execution, you need external tools like concurrently or npm-run-all. These tools let you run multiple watchers, servers, or build steps simultaneously.
| 1 | { |
| 2 | "scripts": { |
| 3 | "dev": "concurrently -n "vite,tsc" -c "green,blue" "vite" "tsc --watch"", |
| 4 | "dev:alt": "npm-run-all --parallel vite tsc:watch", |
| 5 | "build": "npm-run-all --sequential lint typecheck build:vite", |
| 6 | "build:vite": "vite build", |
| 7 | "lint": "eslint src/", |
| 8 | "typecheck": "tsc --noEmit", |
| 9 | "tsc:watch": "tsc --watch --noEmit", |
| 10 | "start": "concurrently -k "node server.js" "npm run dev"", |
| 11 | "ci": "npm-run-all --sequential lint test build" |
| 12 | } |
| 13 | } |
| 1 | # Install concurrency helpers |
| 2 | npm install -D concurrently npm-run-all |
| 3 | |
| 4 | # Run dev server + TypeScript watcher in parallel |
| 5 | npm run dev |
| 6 | |
| 7 | # Run lint, test, build sequentially |
| 8 | npm run ci |
| 9 | |
| 10 | # concurrently with custom name prefixes |
| 11 | concurrently \ |
| 12 | --names "API,WEB,WORKER" \ |
| 13 | --prefix-colors "cyan,magenta,yellow" \ |
| 14 | "npm run dev:api" \ |
| 15 | "npm run dev:web" \ |
| 16 | "npm run dev:worker" |
warning
Arguments after -- are forwarded to the underlying script command. This pattern is essential for tool-specific flags without duplicating script definitions.
| 1 | { |
| 2 | "scripts": { |
| 3 | "test": "vitest", |
| 4 | "test:watch": "vitest --watch", |
| 5 | "lint": "eslint src/", |
| 6 | "build": "vite build" |
| 7 | } |
| 8 | } |
| 1 | # Arguments after -- are forwarded |
| 2 | npm run test -- --run --reporter=verbose |
| 3 | # Executes: vitest --run --reporter=verbose |
| 4 | |
| 5 | npm run lint -- --fix --max-warnings=0 |
| 6 | # Executes: eslint src/ --fix --max-warnings=0 |
| 7 | |
| 8 | npm run build -- --mode=staging |
| 9 | # Executes: vite build --mode=staging |
| 10 | |
| 11 | # Multiple arguments |
| 12 | npm run test -- --run --coverage --reporter=json |
| 13 | # Executes: vitest --run --coverage --reporter=json |
pro tip
Shell commands behave differently on Windows vs Unix. Environment variables use $VAR on Unix but %VAR% on Windows. Path separators, glob patterns, and command syntax all vary. The cross-env package normalizes environment variable setting across platforms.
| 1 | { |
| 2 | "scripts": { |
| 3 | "start:unix": "NODE_ENV=production node server.js", |
| 4 | "start:win": "SET NODE_ENV=production && node server.js", |
| 5 | "start": "cross-env NODE_ENV=production node server.js", |
| 6 | |
| 7 | "build:env": "cross-env NODE_ENV=staging API_URL=https://staging.api.com vite build", |
| 8 | |
| 9 | "test:ci": "cross-env CI=true NODE_OPTIONS=--max-old-space-size=4096 vitest run", |
| 10 | |
| 11 | "clean": "cross-env-shell rimraf dist/ && echo 'Cleaned'" |
| 12 | } |
| 13 | } |
| 1 | # Install cross-env |
| 2 | npm install -D cross-env |
| 3 | |
| 4 | # Without cross-env (Unix only) |
| 5 | NODE_ENV=development node app.js |
| 6 | |
| 7 | # With cross-env (works everywhere) |
| 8 | npx cross-env NODE_ENV=development node app.js |
| 9 | |
| 10 | # Multiple variables |
| 11 | cross-env \ |
| 12 | NODE_ENV=production \ |
| 13 | PORT=8080 \ |
| 14 | LOG_LEVEL=info \ |
| 15 | node app.js |
| 16 | |
| 17 | # Using cross-env-shell for cmd chains |
| 18 | cross-env-shell NODE_ENV=test "npm run lint && npm test" |
Other Cross-Platform Utilities
best practice
Beyond built-in npm scripts, dedicated task runners provide additional features: file watching, incremental builds, dependency graphs between tasks, and plugin systems.
| Tool | Best For | Key Feature |
|---|---|---|
| just | Project-level commands | Simple command runner with recipes |
| make | Multi-language builds | Dependency graph, incremental builds |
| turbo | Monorepo task orchestration | Caching, parallel execution, remote cache |
| nx | Large monorepos | Task graph, affected commands, plugins |
| bun | Speed-critical tasks | Built-in test runner, bun build, Bun.shell |
| 1 | // package.json — using npm scripts directly |
| 2 | { |
| 3 | "scripts": { |
| 4 | "dev": "vite", |
| 5 | "build": "vite build", |
| 6 | "preview": "vite preview", |
| 7 | "lint": "eslint src/", |
| 8 | "test": "vitest run" |
| 9 | } |
| 10 | } |
For most projects, npm scripts with concurrently and cross-env suffice. Reach for turbo or nx when you have a monorepo with 10+ packages and need intelligent caching and task orchestration.
npm provides lifecycle hooks for built-in operations like install, publish, and version. These hooks run automatically and are commonly used for setup scripts, pre-publish checks, and version bump automation.
| 1 | { |
| 2 | "scripts": { |
| 3 | "prepare": "husky || true", |
| 4 | "prepublishOnly": "npm run test && npm run build", |
| 5 | "preversion": "npm run lint", |
| 6 | "version": "npm run build && git add dist/", |
| 7 | "postversion": "git push && git push --tags", |
| 8 | "preinstall": "npx check-engine", |
| 9 | "postinstall": "patch-package" |
| 10 | } |
| 11 | } |
| Hook | Triggered By | Common Use |
|---|---|---|
| prepare | npm install (both local + ci) | Set up git hooks (husky) |
| prepublishOnly | npm publish | Run tests before publishing |
| preversion | npm version | Ensure clean state before bump |
| version | npm version | Update dist files after bump |
| postversion | npm version | Push tags to remote |
| preinstall | npm install | Verify Node.js engine version |
warning