Skip to content

mod_03 - NPM Essentials

npm is the package manager for Node , and it's simultaneously the best and worst thing about the ecosystem
It gives you access to 2+ million packages for everything from web frameworks to "is-odd" (yes, that's a real package with millions of weekly downloads). It also means every project you build depends on hundreds of transitive dependencies written by strangers on the internet. Understanding npm - how it resolves , installs , and validates packages - is non-negotiable for Node security

installing packages

npm install

Adds a package to dependencies in package.json

# Install a package and save to dependencies
npm install express

# Install a specific version
npm install express@4.18.2

# Install from GitHub
npm install github:expressjs/express

# Install a tarball
npm install ./local-package.tgz

npm install --save-dev

Adds a package to devDependencies - things you need for development but not in production

npm install --save-dev jest
npm install -D eslint  # -D is shorthand for --save-dev

Dev dependencies are NOT installed when NODE_ENV=production or when npm install --production is used. This matters for deployment and CI - don't put runtime dependencies in devDependencies

npm install -g

Install a package globally - available as a CLI command system-wide

npm install -g nodemon
npm install -g eslint

Avoid global installs in production. They pollute the system-wide Node installation and make version management a nightmare. Use npx for one-off commands or add to devDependencies and use npm scripts. Global packages are fine for developer tooling on your local machine (nodemon, http-server, typescript)

# Instead of global install:
npm install -g create-react-app
create-react-app my-app

# Use npx:
npx create-react-app my-app

npm ci vs npm install

npm ci is for CI/CD environments - deterministic installs from the lockfile

# Development install - may update lockfile
npm install

# CI install - fails if package.json and lockfile don't match
npm ci

Differences:

  • npm ci deletes node_modules before installing - clean state every time
  • npm ci never modifies package.json or package-lock.json - it uses the lockfile exactly
  • npm ci is faster - it skips dependency resolution and goes straight to installation
  • npm ci fails if package-lock.json is outdated relative to package.json

Use npm ci in CI pipelines. npm install in CI can silently update your lockfile and introduce untested dependency changes

package.json and package-lock.json

package.json

Describes your project - name , version , dependencies , scripts , metadata

{
  "name": "my-app",
  "version": "1.0.0",
  "dependencies": {
    "express": "^4.18.0"
  },
  "devDependencies": {
    "jest": "^29.0.0"
  }
}

This is your manifest. Commit it to version control. The version ranges in dependencies tell npm what versions are acceptable

package-lock.json

Locks every dependency to an exact version - including all transitive dependencies

{
  "name": "my-app",
  "lockfileVersion": 3,
  "packages": {
    "node_modules/express": {
      "version": "4.18.2",
      "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
      "integrity": "sha512-...base64-hash...",
      "dependencies": {
        "accepts": "~1.3.8",
        "array-flatten": "1.1.1"
      }
    }
  }
}

Always commit package-lock.json to version control. It ensures everyone on the team (and your CI/CD pipeline) gets the exact same dependency tree. If you use .npmrc with package-lock=false , you're asking for production-only bugs that only appear because someone's resolver picked a different minor version

semantic versioning

npm uses semver (semantic versioning): MAJOR.MINOR.PATCH

flowchart LR
    V("version 4.18.2") --> MAJ["MAJOR - breaking changes"]
    V --> MIN["MINOR - new features (backward compatible)"]
    V --> PAT["PATCH - bug fixes (backward compatible)"]

Version ranges in package.json:

Range Meaning Example match
1.2.3 Exact version Only 1.2.3
^1.2.3 Same major (minor+patch) 1.2.3 to <2.0.0
~1.2.3 Same minor (patch only) 1.2.3 to <1.3.0
>=1.2.3 Minimum version 1.2.3+
* Any version Everything
latest Latest published Whatever is current
# See what versions are available
npm view express versions

# See what a range resolves to
npm view express@^4.0.0 version

# Check outdated packages
npm outdated

Security rule: Use ^ ranges for most dependencies (you get bug fixes automatically) but pin exact versions for security-critical packages or when you need reproducible zero-day response. For CI , npm ci ignores ranges and uses the lockfile anyway

npm audit and security

npm audit

Scans your dependency tree for known vulnerabilities

# Regular audit
npm audit

# Fix vulnerabilities automatically (may update lockfile and package.json)
npm audit fix

# Audit for production dependencies only
npm audit --production

npm audit compares your dependencies against the npm Advisory database - CVEs reported to npm. It categorizes vulnerabilities by severity: critical , high , moderate , low

# Example output
# === npm audit security report ===
#
# moderate        Prototype Pollution in lodash
# Package:        lodash
# Dependency of:  express
# Path:           express > lodash
# More info:      https://github.com/advisories/GA-xxxx
#
# fix available via `npm audit fix`

What npm audit can't do: find vulnerabilities in your own code , detect malicious packages that aren't reported to npm , or check runtime behavior

npm doctor

Runs diagnostics on your npm environment

npm doctor

# Checks:
# - npm version is current
# - Node.js version is compatible
# - Registry connectivity
# - Permissions on node_modules
# - Git access

npm verify

Checks the integrity of installed packages against the hashes in your lockfile

# Verify integrity of all installed packages
npm cache verify

# Recalculate integrity hashes
npm rebuild

npm signature verification

Node 20+ includes experimental support for verifying package signatures

# Configure npm to require signatures
npm config set sign-git-tag true

# Audit signatures (Node 20+)
npm audit signatures

package resolution and hoisting

npm installs packages in node_modules with a nested tree structure

flowchart TD
    root["my-app/"] --> nm["node_modules/"]
    nm --> exp["express/<br/>direct dependency"]
    exp --> nm2["node_modules/"]
    nm2 --> acc["accepts/<br/>express's dependency (nested)"]
    nm --> lod["lodash/<br/>hoisted to top level"]

Modern npm (v7+) uses the node_modules hoisting algorithm to minimize nesting. If multiple packages depend on different versions of the same package , npm nests the incompatible versions

# See the actual resolved tree
npm ls

# See why a specific version is installed
npm explain express

summary

  • npm install adds to dependencies; --save-dev adds to devDependencies
  • Use npm ci in CI/CD for deterministic installs from lockfile
  • Always commit package-lock.json to version control
  • Semver ranges: ^ for minor+patch updates , ~ for patch-only
  • Run npm audit regularly and fix vulnerabilities
  • Avoid global installs - use npx instead
  • npm ls shows the resolved dependency tree
  • Every dependency is a supply chain risk - audit before you install

prerequisites

mod_02_es_modules.md - ES module syntax

next -> mod_04_package_json.md