npm Basics: Installing & Managing Packages
npm is the default package manager that ships with every Node.js installation, and it is the tool you use to pull third-party code into your project, keep it up to date, and remove it when it is no longer needed. Almost every non-trivial Node application depends on packages from the npm registry, so understanding how npm install works — and what it does to your node_modules folder and package.json — is foundational. This page covers installing, updating, and uninstalling packages, the difference between local and global installs, and how the dependency tree is laid out on disk.
Installing packages
The core command is npm install (aliased as npm i). Running it with a package name fetches that package from the registry, writes it into node_modules, and records it in your package.json. Running it with no arguments installs everything already listed in package.json — this is what you do after cloning a repository.
# Install a single package and add it to dependencies
npm install express
# Shorthand
npm i express
# Install everything declared in package.json (e.g. after git clone)
npm install
# Install a specific version
npm install [email protected]
After installation, npm updates the dependencies section of package.json and writes an exact, reproducible tree into package-lock.json.
Output:
added 65 packages, and audited 66 packages in 2s
10 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Local vs global installs
By default, packages install locally — into the node_modules folder of the current project. Local packages are what your application’s import and require statements resolve against, and they are scoped to that one project. This is what you want for runtime dependencies and libraries.
A global install (-g) places a package in a system-wide location and is intended for command-line tools you want available from any directory, such as npm-distributed CLIs.
# Local — used by this project's code
npm install lodash
# Global — installs a CLI binary onto your PATH
npm install -g http-server
| Aspect | Local (default) | Global (-g) |
|---|---|---|
| Location | Project node_modules | System-wide prefix |
| Recorded in package.json | Yes | No |
Used by import/require | Yes | No |
| Typical use | Libraries, frameworks | Standalone CLI tools |
Prefer a local install plus
npxover a global install for project tooling.npx eslintruns the version pinned in your project, keeping every contributor on the same tool version instead of relying on whatever each machine installed globally.
Dependencies vs devDependencies
Packages your application needs at runtime belong in dependencies. Packages needed only during development — test runners, bundlers, type definitions, linters — belong in devDependencies, declared with --save-dev (-D). This separation matters because production installs can skip devDependencies entirely.
# Runtime dependency
npm install zod
# Development-only dependency
npm install --save-dev vitest
# Shorthand
npm i -D typescript
Installing with --omit=dev (the modern replacement for --production) installs only the dependencies, which is exactly what you want in a production container build:
npm install --omit=dev
Updating packages
Dependencies in package.json use semantic-version ranges (e.g. ^4.19.0), so they are allowed to move within a compatible band. npm outdated shows you which packages have newer versions available, and npm update upgrades them within the range your package.json permits.
# See what's behind
npm outdated
# Update packages to the newest version allowed by your ranges
npm update
# Update a single package
npm update express
Output:
Package Current Wanted Latest Location Depended by
express 4.18.2 4.19.2 5.1.0 node_modules/... my-app
zod 3.22.4 3.23.8 3.23.8 node_modules/... my-app
The Wanted column is the highest version satisfying your range (what npm update installs); Latest is the absolute newest published version. Jumping to a new major (Latest when it exceeds Wanted) requires changing the range yourself — npm install express@5 — because major versions may contain breaking changes.
Removing packages
npm uninstall (aliased npm remove, npm rm) deletes a package from node_modules and removes its entry from package.json and package-lock.json.
npm uninstall lodash
# Remove a global tool
npm uninstall -g http-server
How node_modules and the dependency tree work
When you install a package, npm also installs everything that package depends on, and their dependencies, and so on — the full transitive tree. Rather than nesting every dependency inside its parent, modern npm flattens (hoists) packages to the top level of node_modules whenever versions are compatible. This deduplicates shared dependencies and shortens paths.
node_modules/
├── express/
├── accepts/ ← hoisted dependency of express
├── body-parser/ ← hoisted dependency of express
└── debug/ ← shared by several packages, installed once
When two packages need incompatible versions of the same dependency, npm keeps the conflicting copy nested inside the package that requires it, so both versions coexist safely. The resolver’s node_modules walk-up (covered in Module Resolution) is what makes both the hoisted and nested copies findable.
Never commit
node_modulesto version control, and never edit files inside it. It is a generated, machine-specific artifact. Commitpackage.jsonandpackage-lock.jsoninstead, and letnpm install(ornpm ciin CI) rebuild the tree reproducibly.
Best practices
- Commit
package.jsonandpackage-lock.json; addnode_modules/to.gitignore. - Use
--save-devfor tooling so production installs (--omit=dev) stay lean. - Run
npm ciinstead ofnpm installin CI and Docker builds for exact, lockfile-driven installs. - Review
npm outdatedperiodically and update inside your semver ranges withnpm update; bump majors deliberately and test. - Run
npm auditafter installs to surface known vulnerabilities, and pin exact versions for tooling where reproducibility matters most. - Prefer local installs with
npxover global installs to keep tool versions consistent across machines.