package.json Scripts
npm lifecycle scripts automate build, test, and publish steps. They are the contract between developers, CI, and deployment pipelines.
Recipe
Quick-reference recipe card - copy-paste ready.
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc -p tsconfig.build.json",
"start": "node dist/index.js",
"test": "node --import tsx --test test/**/*.test.ts",
"typecheck": "tsc --noEmit",
"prepare": "npm run build",
"prepublishOnly": "npm test && npm run typecheck"
}
}When to reach for this:
- You need one command (
npm test) that works locally and in CI. - Git installs or
npm publishmust compile TypeScript before consumers import the package. - You want guardrails that block broken publishes without remembering manual steps.
Working Example
{
"name": "@acme/billing-api",
"version": "1.4.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": ["dist"],
"scripts": {
"dev": "tsx watch src/server.ts",
"build": "tsc -p tsconfig.build.json",
"start": "node dist/server.js",
"test": "node --import tsx --test",
"typecheck": "tsc --noEmit",
"lint": "eslint .",
"prepare": "npm run build",
"prepublishOnly": "npm run lint && npm test && npm run typecheck",
"postversion": "git push && git push --tags"
}
}# Local development
npm run dev
# CI pipeline (same entry points)
npm ci
npm run lint
npm run typecheck
npm test
npm run buildWhat this demonstrates:
devusestsx watchfor fast TypeScript iteration without a separate compile step.preparebuilds onnpm installwhen the package is installed from git or packed tarball.prepublishOnlyruns only beforenpm publish, catching regressions before they hit the registry.postversionautomates tag pushes afternpm version patch.
Deep Dive
How It Works
- npm runs scripts by name:
npm run <script>ornpm testfor thetestalias. - Lifecycle hooks (
prepare,prepublishOnly,postinstall) fire automatically at defined moments. - Scripts inherit
PATHwithnode_modules/.binprepended, so local CLIs resolve withoutnpx. npm runsetsnpm_lifecycle_eventso scripts can branch on the triggering hook.
Common Lifecycle Hooks
| Hook | When it runs | Typical use |
|---|---|---|
prepare | After npm install (local and packed) | Compile TypeScript, generate types |
prepublishOnly | Before npm publish | Tests, lint, build verification |
postinstall | After dependency install | Native addon builds (use sparingly) |
preversion / postversion | Around npm version | Changelog, git tag push |
TypeScript Notes
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc -p tsconfig.build.json",
"typecheck": "tsc --noEmit -p tsconfig.json"
}
}- Keep
typecheckseparate frombuildso CI can fail fast without emittingdist/. - Use
tsxfor dev; ship compiled JS indist/for productionnode dist/....
Gotchas
- Infinite prepare loops -
preparecallingnpm installor reinstalling triggers itself. Fix: only compile or copy files inprepare. - Heavy postinstall - Slow installs frustrate every developer and CI job. Fix: move optional setup to documented
npm run setup. - Skipping tests on publish -
npm publish --ignore-scriptsbypassesprepublishOnly. Fix: enforce CI publish from tagged commits with script checks. - Cross-platform env vars -
NODE_ENV=production cmdfails on Windows shells. Fix: usecross-envor framework-specific flags. - Implicit prepare on git deps - Installing from GitHub runs
prepare, which can surprise consumers. Fix: publish prebuilt artifacts to npm instead of git URLs for libraries.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
Makefile / just | Polyglot repos, ops-heavy workflows | Team expects npm test everywhere |
| Turborepo task pipeline | Monorepos with cached build/test | Single-package API service |
| Husky + lint-staged | Pre-commit formatting only | Replacing CI gates entirely |
FAQs
What is the difference between prepare and prepublishOnly?
prepareruns onnpm install(including git dependencies) and before pack/publish.prepublishOnlyruns only immediately beforenpm publish.- Use
preparefor builds consumers need; useprepublishOnlyfor publish-only checks.
Can I run multiple commands in one script?
{
"scripts": {
"check": "npm run lint && npm run typecheck && npm test"
}
}Chain with && so later steps skip if an earlier step fails.
How do I pass arguments to a script?
npm test -- --grep "billing"Arguments after -- are forwarded to the underlying command.
Should start run tsx or compiled JS?
- Production
startshould runnode dist/...afterbuild. - Development uses
tsx watchvia a separatedevscript.
Why does npm run build on npm install in my library?
prepare runs after install when the package is installed from git or a local path. Publish built dist/ to npm or document the compile step.
How do I silence lifecycle output in CI?
Use --ignore-scripts only when you fully control what is skipped. Prefer explicit npm run build in CI instead of relying on side effects.
Can scripts call other package scripts?
Yes: "check": "npm run lint && npm run test". npm resolves local binaries automatically.
What runs before npm version?
preversion (optional), then version bump, then postversion. Use postversion to push tags.
Should I use npx inside scripts?
Prefer devDependencies (tsx, eslint) and bare command names (tsx, eslint) so versions are lockfile-pinned.
How do monorepos organize scripts?
Root package.json delegates: "test": "turbo run test". Each workspace keeps its own build/test scripts.
Related
- Lockfiles & Reproducible Installs - CI uses the same install path
- Publishing to npm - semver and publish hygiene
- Package Managers Basics - manager selection
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.