Most developers type:

npm install 

and think:

“npm is downloading my dependencies.”

That's only part of the story.

Depending on your project and dependencies, an install can involve dependency resolution, lockfile processing, lifecycle scripts, native builds, transitive dependencies, and executable code.

And that's why npm install deserves more attention than it usually gets.

1. npm install Doesn't Just Download Packages

Consider:

npm install express 

At a high level, npm needs to:

Read your package.json
Resolve dependency versions
Read or update the lockfile
Download packages
Install dependencies
Resolve transitive dependencies
Run applicable lifecycle scripts
Potentially build native dependencies

So this:

npm install 

isn't equivalent to:

Download files 

It's closer to:

Resolve ↓ Download ↓ Install ↓ Execute lifecycle scripts ↓ Build where necessary 

That distinction matters.

2. Your Dependencies Have Dependencies

You might install:

npm install some-library 

and see:

node_modules/ └── some-library 

But internally, the dependency tree could look more like:

your-app │ ├── some-library │ ├── dependency-a │ │ └── dependency-x │ └── dependency-b │ └── dependency-y │ └── another-library └── dependency-c 

You explicitly selected only one package.

But you're potentially installing dozens or hundreds of packages indirectly.

These are called transitive dependencies.

That's one reason dependency management is also a supply-chain problem.

3. The Package You Install Isn't the Only Code You Trust

Imagine:

npm install package-a 

You inspect package-a.

It looks perfectly reasonable.

But:

package-a ↓ package-b ↓ package-c ↓ package-d 

Your application now depends on all of them.

This creates a trust chain:

Your code ↓ Direct dependency ↓ Transitive dependency ↓ Another dependency ↓ Another dependency 

You might never have looked at the source code of the last package.

Yet it can still become part of your application's dependency tree.

4. The Part Developers Often Forget: Lifecycle Scripts

Here's where things get particularly interesting.

A package can define scripts in its package.json:

{ "scripts": { "postinstall": "node setup.js" } } 

When that package is installed, npm may execute its lifecycle script.

For example:

npm install 

can result in:

Resolve dependencies ↓ Download package ↓ Install package ↓ Run lifecycle script 

So an installation can involve executing code, not merely copying JavaScript files into node_modules.

5. What Are Lifecycle Scripts?

npm supports various lifecycle events.

Some commonly encountered ones include:

preinstall install postinstall prepare prepublish 

For example:

{ "scripts": { "postinstall": "node scripts/setup.js" } } 

The script might legitimately:

  • compile native components
  • generate files
  • prepare a package
  • configure tooling

But from a security perspective, the important point is:

Installing a dependency can cause code to run during installation.

That's why blindly installing packages isn't ideal.

6. Is This Automatically Dangerous?

No.

This is important.

Lifecycle scripts are a legitimate npm feature and are used by many legitimate packages.

For example, packages with native components may need installation-time scripts to prepare binaries.

The problem isn't:

“npm scripts are malicious.”

The problem is:

You're executing code from your dependency chain, so you should understand what you're trusting.

That's a much more accurate security mindset.

7. Transitive Dependencies Are the Bigger Problem

Imagine your application has:

{ "dependencies": { "framework-x": "^5.0.0" } } 

You might think:

“I only have one dependency.”
Not really.

That package could bring:

framework-x ├── library-a │ ├── library-c │ └── library-d ├── library-b │ └── library-e └── library-f 

Your application may now depend on many packages.

This is why modern applications can end up with surprisingly large dependency trees.

8. That's Where package-lock.json Matters

When npm resolves your dependencies, it needs to know exactly which versions should be installed.

That's where:

package-lock.json 

comes in.

It records the resolved dependency tree and package information so that installs can be reproduced more consistently.

For example:

package.json ↓ Version ranges ↓ package-lock.json ↓ Resolved versions 

Without a lockfile, two installations at different times can potentially resolve different versions within the ranges allowed by package.json.

9. npm install vs npm ci

This is one of the most important distinctions for CI/CD.

npm install

Generally used when you're:

  • adding dependencies
  • updating dependencies
  • working locally
  • changing the dependency tree

It can modify the lockfile when dependency resolution changes.

npm ci

Designed for clean, reproducible installs in automated environments.

Typically:

npm ci 

expects the lockfile to be consistent with package.json and installs from the lockfile rather than resolving a fresh dependency tree in the same way as a normal npm install.

That's why you'll commonly see:

steps: - run: npm ci - run: npm test - run: npm run build 

in CI workflows.

10. Why npm ci Is Better for CI

Imagine your team has:

Developer A ↓ npm install ↓ works CI server ↓ npm install ↓ different dependency resolution ↓ unexpected failure 

Using a committed lockfile with:

npm ci 

helps make the CI installation deterministic with respect to the lockfile.

A common production workflow is:

package.json + package-lock.json ↓ npm ci ↓ Build ↓ Test ↓ Deploy 

11. What Happens If a Dependency Is Compromised?

This is where dependency security becomes serious.

Imagine:

Your application ↓ library-a ↓ library-b ↓ compromised-package 

You didn't intentionally install:

compromised-package 

But it entered your dependency tree through another package.

Depending on the package and its capabilities, malicious code could potentially:

  • execute during installation
  • modify generated files
  • access the environment available to the install process
  • compromise build artifacts
  • attempt to access credentials or secrets available to the process

The exact impact depends on the package, permissions, scripts, and environment.

But the principle is simple:

Your dependency tree is part of your software supply chain.

12. Don't Put Secrets Into the Install Environment Without Thinking

Suppose your CI system exposes secrets:

NPM_TOKEN AWS credentials deployment credentials API keys 

and then runs:

npm ci 

Remember:

dependencies are being installed in that environment.

If a malicious installation script executes, the security boundary matters.

That's why CI environments should follow the principle of least privilege.

For example:

Build job ↓ Only required credentials ↓ Only required permissions 

Not:

Build job ↓ Every production secret ↓ Full cloud access 

13. You Can Disable Lifecycle Scripts When Appropriate

npm provides:

npm install --ignore-scripts 

and:

npm ci --ignore-scripts 

This tells npm not to run package lifecycle scripts.

That can be useful in security-sensitive environments when your dependencies don't require installation scripts.

But don't blindly add it everywhere.

Some packages legitimately depend on lifecycle scripts to:

  • build native components
  • generate required files
  • prepare packages

So first understand your dependency requirements.

14. Don't Blindly Copy --ignore-scripts Into Production

You might see:

npm ci --ignore-scripts 

and think:

“Great. Security solved.”
No.

You have simply changed the installation behavior.

A package might require:

postinstall 

to generate something necessary.

Your build could then fail—or worse, appear to work while a required step was skipped.

Security improvements should be tested against your actual dependency tree.

15. Audit Your Dependencies

npm provides:

npm audit 

This can identify known vulnerabilities in your dependency tree.

For example:

npm audit 

You may get information about:

package severity vulnerable versions patched versions dependency path 

The useful part isn't just seeing:

“12 vulnerabilities found.”

You need to understand:

Why is this package installed? Who depends on it? Is the vulnerable code actually reachable? Is there a patched version? Will upgrading break something? 

Security tooling is a starting point—not a replacement for engineering judgment.

16. Understand Where a Dependency Came From

When you find a suspicious package, inspect the tree:

npm ls package-name 

This can help answer:

“Why is this package in my project?”

For example:

your-app └── framework-a └── library-b └── package-name 

Now you know that removing package-name directly might not be the right solution.

You may need to update:

framework-a 

or:

library-b 

instead

17. Use npm explain

Another useful command is:

npm explain package-name 

It helps explain why a package exists in your dependency tree.

This is especially useful when your project has hundreds or thousands of transitive dependencies.

Instead of asking:

“Why is this package here?”

you can inspect its dependency path.

18. Don't Install Packages Just Because a Tutorial Does

This happens all the time:

npm install some-random-package 

because a blog post or Stack Overflow answer suggested it.

Before installing a package, consider:

Is it maintained?
Look at:

  • recent releases
  • repository activity
  • issue activity

Does it have a reasonable dependency footprint?

A tiny utility pulling in dozens of dependencies deserves a second look.

Is it actually necessary?

Sometimes:

array.flat() 

is enough.

You don't necessarily need another package.

19. Fewer Dependencies Can Mean Less Risk

Imagine two implementations.

Option A

Your app ↓ 3 dependencies ↓ 17 transitive dependencies 

Option B

Your app ↓ 12 direct dependencies ↓ 400 transitive dependencies 

More dependencies aren't automatically bad.

But every dependency introduces:

  • maintenance
  • updates
  • compatibility concerns
  • vulnerability exposure
  • supply-chain considerations

So ask:

Does this dependency provide enough value to justify adding it?

20. Be Careful With Dependency Updates

This command:

npm update 

can change resolved versions within the constraints specified by your dependency declarations.

Dependency updates should be treated as changes to your software, not as harmless maintenance.

A good process is:

Update ↓ Install ↓ Run tests ↓ Run security checks ↓ Review lockfile changes ↓ Deploy 

Not:

npm update ↓ Ship directly to production 

21. Review Your Lockfile Changes

Suppose a pull request changes:

package.json package-lock.json 

Don't automatically approve it because:

“It's just dependency updates.”

The lockfile can contain hundreds of changed lines.

Look at:

  • packages added
  • packages removed
  • version changes
  • unexpected dependency changes

A dependency update can have a much larger impact than the two-line change in package.json suggests.

22. A Better CI Workflow

For a typical Node.js application, you might have:

steps: - checkout - run: npm ci - run: npm run lint - run: npm test - run: npm run build 

You can additionally incorporate dependency/security checks appropriate to your project.

The important principle is:

Clean install ↓ Validate ↓ Test ↓ Build ↓ Deploy 

Don't make the build environment unnecessarily privileged.

23. A Practical npm Security Checklist

Before installing a new dependency:

☑ Is it actually necessary? ☑ Is the package maintained? ☑ Who publishes it? ☑ What dependencies does it bring? ☑ Does it use lifecycle scripts? ☑ Is it receiving security updates? ☑ Is the package name definitely correct? 

For your repository:

☑ Commit package-lock.json ☑ Use npm ci in CI where appropriate ☑ Review dependency changes ☑ Run npm audit/security tooling ☑ Keep dependencies updated ☑ Minimize CI credentials ☑ Avoid unnecessary packages ☑ Investigate unexpected transitive dependencies 

24. The Bigger Lesson

The biggest mistake is thinking:

npm install = download dependencies.

A better mental model is:

npm install │ ├── Resolve dependencies │ ├── Read/update lockfile │ ├── Download packages │ ├── Install transitive dependencies │ ├── Run applicable lifecycle scripts │ └── Potentially build native components 

That's a lot more than downloading files.

Final Takeaway

Modern JavaScript applications depend on huge ecosystems.

That's incredibly powerful.

But it also means your application isn't just:

Your code 

It's closer to:

Your code ↓ Direct dependencies ↓ Transitive dependencies ↓ Build tools ↓ CI environment ↓ Production artifacts 

Every layer matters.

So the next time you type:

npm install 

remember:

You're not just installing code. You're extending your software supply chain.

And that's why dependency management should be treated as part of development, reliability, and security—not just package installation.