npm — Package Lifecycle & Hooks
npm provides a powerful system of lifecycle scripts that run automatically at specific points during package installation, publishing, and development. Understanding the lifecycle is crucial for setting up build steps, database migrations, and other automation.
This guide covers lifecycle script order, pre/post hooks, npm link for local development, npx, and npm cache management.
npm runs lifecycle scripts in a specific order when you run npm install, npm publish, and other commands. Understanding this order helps you choose the right hook for your automation.
| Order | Script | Runs During |
|---|---|---|
| 1 | preinstall | Before installation starts |
| 2 | install | After preinstall, during installation |
| 3 | postinstall | After installation completes |
| 4 | prepublish | Before packing and publishing (deprecated) |
| 5 | prepare | Before packing, after npm install, on git pull |
| 6 | prepublishOnly | Before publishing (replaces prepublish) |
| 7 | prepack | Before tarball is created |
| 8 | postpack | After tarball is created |
| 9 | publish | After publish completes |
| 10 | postpublish | After publish completes |
| 1 | # npm install lifecycle: |
| 2 | # 1. preinstall |
| 3 | # 2. install |
| 4 | # 3. postinstall |
| 5 | # 4. prepare (if git or npm link) |
| 6 | |
| 7 | # npm publish lifecycle: |
| 8 | # 1. prepublishOnly |
| 9 | # 2. prepare |
| 10 | # 3. prepack |
| 11 | # 4. pack (creates tarball) |
| 12 | # 5. postpack |
| 13 | # 6. publish |
| 14 | # 7. postpublish |
| 15 | |
| 16 | # npm pack lifecycle: |
| 17 | # 1. prepack |
| 18 | # 2. pack |
| 19 | # 3. postpack |
npm automatically provides pre and post hooks for any script you define. If you create a script named build, npm will automatically run prebuild before it and postbuild after it.
| 1 | { |
| 2 | "scripts": { |
| 3 | "prebuild": "rimraf dist", |
| 4 | "build": "tsc", |
| 5 | "postbuild": "cp package.json dist/", |
| 6 | "pretest": "eslint src/", |
| 7 | "test": "vitest run", |
| 8 | "posttest": "echo 'Tests complete'", |
| 9 | "predeploy": "npm run build", |
| 10 | "deploy": "aws s3 sync dist/ s3://my-app", |
| 11 | "postdeploy": "npm run healthcheck" |
| 12 | } |
| 13 | } |
| 14 | |
| 15 | // Running npm run build executes: |
| 16 | // 1. prebuild -> rimraf dist |
| 17 | // 2. build -> tsc |
| 18 | // 3. postbuild -> cp package.json dist/ |
| 19 | |
| 20 | // Running npm test executes: |
| 21 | // 1. pretest -> eslint src/ |
| 22 | // 2. test -> vitest run |
| 23 | // 3. posttest -> echo 'Tests complete' |
info
The prepare and prepublishOnly scripts are often confused. They serve different purposes:
| Script | When It Runs | Common Use |
|---|---|---|
| prepare | npm install, npm pack, npm publish, git pull (with husky) | Build step, install husky hooks, compile TypeScript |
| prepublishOnly | Only during npm publish | Run tests, lint, verify before publishing |
| 1 | { |
| 2 | "scripts": { |
| 3 | "prepare": "husky && tsc", |
| 4 | "prepublishOnly": "npm test && npm run lint" |
| 5 | } |
| 6 | } |
| 7 | |
| 8 | // prepare runs during: |
| 9 | // - npm install (after dependencies are installed) |
| 10 | // - npm link (after symlink is created) |
| 11 | // - npm publish (before prepublishOnly) |
| 12 | // This makes it ideal for: |
| 13 | // - Setting up Git hooks (husky) |
| 14 | // - Building TypeScript to JavaScript |
| 15 | // - Generating browser bundles |
| 16 | |
| 17 | // prepublishOnly runs only during: |
| 18 | // - npm publish (after prepare, before packing) |
| 19 | // Use it for: |
| 20 | // - Running test suite |
| 21 | // - Linting |
| 22 | // - Security audit |
| 23 | // - Any check that should block publishing |
best practice
npm link creates a symlink between a local package and a project, allowing you to develop and test a package locally without publishing to the registry.
| 1 | # Step 1: In your package directory, create a global symlink |
| 2 | cd ~/projects/my-lib |
| 3 | npm link |
| 4 | # This creates: /usr/local/lib/node_modules/my-lib -> ~/projects/my-lib |
| 5 | |
| 6 | # Step 2: In your project, link to the package |
| 7 | cd ~/projects/my-app |
| 8 | npm link my-lib |
| 9 | # This creates: node_modules/my-lib -> /usr/local/lib/node_modules/my-lib -> ~/projects/my-lib |
| 10 | |
| 11 | # Now changes in ~/projects/my-lib are immediately reflected in my-app |
| 12 | |
| 13 | # Step 3: When done, unlink |
| 14 | cd ~/projects/my-app |
| 15 | npm unlink my-lib |
| 16 | # OR |
| 17 | npm uninstall --no-save my-lib && npm install |
| 18 | |
| 19 | # Step 4: Remove the global symlink |
| 20 | cd ~/projects/my-lib |
| 21 | npm unlink |
| 22 | |
| 23 | # Alternative: direct link (Node.js 14+) |
| 24 | cd ~/projects/my-app |
| 25 | npm link ../my-lib |
| 26 | # Creates the symlink in one step without global registration |
warning
npx (bundled with npm 5.2+) executes Node packages without globally installing them. It downloads the package to a cache, runs it, and discards it — keeping your system clean.
| 1 | # Run a one-off command without installing |
| 2 | npx create-react-app my-app |
| 3 | npx tsc --init |
| 4 | npx prettier --write src/ |
| 5 | |
| 6 | # Use a specific version |
| 7 | npx cowsay@1.5.0 "Hello" |
| 8 | |
| 9 | # Run from a local package |
| 10 | npx mocha tests/ |
| 11 | |
| 12 | # Check if npx is available |
| 13 | npx --version |
| 14 | |
| 15 | # Where npx caches packages |
| 16 | ls ~/.npm/_npx/ |
| 17 | |
| 18 | # Force re-download (skip cache) |
| 19 | npx --ignore-existing create-react-app my-app |
| 20 | |
| 21 | # npx is great for: |
| 22 | # - One-off tools (create-react-app, degit) |
| 23 | # - Running different versions of the same tool |
| 24 | # - CI scripts without global installs |
| 25 | # - Trying a package before committing to it |
| 26 | |
| 27 | # npm 9+ alias: |
| 28 | npm x cowsay "Hello" # same as npx |
pro tip
npm caches downloaded packages to speed up subsequent installations. The cache is stored in ~/.npm/_cacache/ and can be verified, cleaned, and inspected.
| 1 | # Verify cache integrity |
| 2 | npm cache verify |
| 3 | # Rebuilds cache index and verifies all cached tarballs |
| 4 | |
| 5 | # Clean the entire cache |
| 6 | npm cache clean --force |
| 7 | # WARNING: This removes everything. Next install will be slow. |
| 8 | |
| 9 | # Cache location (macOS/Linux): |
| 10 | ls ~/.npm/_cacache/ |
| 11 | |
| 12 | # Cache content verification: |
| 13 | npm cache ls |
| 14 | # Lists all cached packages |
| 15 | |
| 16 | # Check cache size: |
| 17 | du -sh ~/.npm/_cacache/ |
| 18 | |
| 19 | # Cache behavior: |
| 20 | # - npm stores tarballs in content-addressable storage |
| 21 | # - Cache is content-addressable (same tarball = one copy) |
| 22 | # - npm ci uses cache heavily for fast installs |
| 23 | # - Cache is self-healing (npm cache verify fixes issues) |
| 24 | |
| 25 | # Best practice: Don't clear the cache unless |
| 26 | # you're debugging corruption issues. |
| 27 | # Use npm cache verify instead of cache clean. |
info
npm ci (clean install) is designed for CI/CD environments. It installs dependencies directly from package-lock.json without resolving ranges, making it faster and more deterministic.
| 1 | # CI install (fast, deterministic) |
| 2 | npm ci |
| 3 | |
| 4 | # Key differences from npm install: |
| 5 | # - Deletes node_modules before installing |
| 6 | # - Uses package-lock.json exactly (no resolution) |
| 7 | # - Fails if package-lock.json is out of sync with package.json |
| 8 | # - Does not write to package-lock.json |
| 9 | # - Faster (skips dependency resolution) |
| 10 | # - Installs exact versions from lock file |
| 11 | |
| 12 | # Typical CI workflow: |
| 13 | # 1. npm ci (restore dependencies exactly) |
| 14 | # 2. npm run build |
| 15 | # 3. npm test |
| 16 | |
| 17 | # CI is 2-10x faster than npm install for large projects |
| 18 | # because it skips resolution and uses the cache. |
| 19 | |
| 20 | # If package-lock.json is missing or outdated: |
| 21 | # npm ci will FAIL with an error message. |
| 22 | # Solution: run npm install locally and commit the lock file. |
best practice
npm pack creates a tarball of your package exactly as it would be published. This is useful for testing the publish artifact locally before actually publishing.
| 1 | # Create a tarball |
| 2 | npm pack |
| 3 | # Creates: my-package-1.0.0.tgz |
| 4 | |
| 5 | # Verify the tarball contents |
| 6 | tar -tzf my-package-1.0.0.tgz |
| 7 | |
| 8 | # Install from tarball in another project |
| 9 | npm install ../my-lib/my-package-1.0.0.tgz |
| 10 | |
| 11 | # Pack with a specific version |
| 12 | npm pack --pack-destination ./output |
| 13 | |
| 14 | # Compare with npm publish dry-run: |
| 15 | npm publish --dry-run |
| 16 | # Shows what would be published without actually publishing |
| 17 | |
| 18 | # npm pack is useful for: |
| 19 | # - Verifying package.json files field |
| 20 | # - Checking .npmignore behavior |
| 21 | # - Testing package size |
| 22 | # - Local integration testing |
| 23 | # - CI artifact caching |
| 24 | |
| 25 | # Check package size: |
| 26 | npm pack --dry-run | wc -c |
| 27 | ls -lh my-package-*.tgz |
Use prepare for Build Steps
Put your build command in prepare to ensure it runs after npm install and before npm publish. This guarantees your package is always built.
Always Use npm ci in CI
npm ci is faster and more deterministic than npm install. It also catches the common mistake of forgetting to commit package-lock.json.
Don't Clear Cache Unless Necessary
The npm cache is content-addressable and self-healing. If you suspect corruption, run npm cache verify instead of npm cache clean --force.
npx Over Global Installs
Prefer npx for running one-off CLI tools. It keeps your global environment clean and ensures you always use the correct version for each project.