npm 08 ๐ Global vs Local Package Installation (-g flag)
npm installs packages in two locations, and the flag determines which one. A local install, the default, places the package in the node_modules folder of the current project and records it in package.json. A global install, triggered by -g or --global, places the package in a system-wide location (/usr/local/lib/node_modules on most Unix systems) and makes its executables available on the PATH . The distinction is not about preference; it is about what the package is for. A library that your code require()s must be installed locally, because Node’s module resolution searches the local node_modules tree, not the global one . A command-line tool that you run from any directory must be installed globally, because otherwise its binary is buried in a project-specific .bin folder that is only on the PATH during that project’s npm scripts .
The practical rule from the npm documentation is direct: “Install it locally if you’re going to require() it. Install it globally if you’re going to run it on the command line. If you need both, then install it in both places, or use npm link” . The npm link escape hatch creates a symbolic link from the global install to the local node_modules, so a single package can serve as both a library and a CLI tool without duplicating the installation. This matters for packages like express or coffee-script that ship both a library API and a command-line interface.
The global install location depends on the prefix configuration. On most Unix systems, prefix is /usr/local, so global packages go to /usr/local/lib/node_modules and their binaries are symlinked to /usr/local/bin . On Windows, the prefix is %AppData%\npm, and the layout differs slightly: global packages go to {prefix}/node_modules without the lib folder . The npm root -g command prints the global package directory, and npm bin -g prints the binary directory, which is useful for shell scripts and debugging . A global install may require sudo on Unix systems if the prefix directory is not writable by the current user, which is a common source of permission problems. The recommended fix is to configure npm to use a user-owned prefix rather than running global installs with sudo.
The ecosystem has shifted toward reducing global installs. The Node.js documentation states that “in general, all packages should be installed locally” because local installs allow different projects to use different versions of the same package, while a global update changes the version for every project that uses it . The npx command (bundled with npm since v5.2.0) runs a package from the registry or from a local node_modules/.bin without installing it globally, which covers the use case of a one-off CLI invocation. The GitHub community discussion on the topic recommends local installs for project dependencies and global installs only for CLI tools used across many projects, with npx as the modern alternative for tools that do not need to be permanently installed .
Key point: Local installs are for libraries your code imports; global installs are for CLI tools you run from the shell. The global location is {prefix}/lib/node_modules on Unix, and the binary is symlinked to {prefix}/bin. When a package is both a library and a CLI, install it locally and use npx to run its binary, or install it globally and use npm link to expose it locally.
Why two installation modes exist
Node’s module resolution is local-first. When code calls require('lodash'), Node walks up from the current file’s directory looking for a node_modules/lodash folder. It does not search the global node_modules by default. A globally installed library is invisible to require() unless the application explicitly adds the global path to NODE_PATH, which is a brittle and discouraged workaround . This is why a package that the application imports must be installed locally: the module resolution algorithm is designed for project-scoped dependencies, not system-wide ones.
The PATH is the global namespace. A global install’s primary effect is symlinking the package’s executables into {prefix}/bin, which is typically on the shell’s PATH . When you type create-react-app or nodemon in a terminal, the shell finds the symlink in /usr/local/bin and executes the package’s entry point. A local install also creates executables, but they go into ./node_modules/.bin, which is only added to the PATH during npm script execution . Outside an npm script, a locally installed CLI tool is not on the PATH; you must invoke it with npx or a relative path.
Global installs create version conflicts across projects. A global package has exactly one version on the system. If project A requires typescript@4.9 and project B requires typescript@5.3, a global install can satisfy only one of them. The other project will either fail or use the wrong version. Local installs avoid this by allowing each project to have its own node_modules with its own version of every dependency . This is the core reason the Node.js documentation recommends local installs as the default: the isolation of project dependencies is the foundation of reproducible builds.
The trade-off. Global installs are convenient for tools that are genuinely globalโa formatter, a linter, a scaffolding CLIโbecause they are available from any directory without npx or a project-specific setup. The cost is version lock-in and the risk of a global update breaking a tool that a project depends on. The npx command has reduced the need for global installs by running a package without installing it, but it adds a small startup cost and requires network access if the package is not cached. The choice depends on how frequently the tool is used and whether its version needs to be pinned per project.
a. Local installs: the default and the module resolution path
npm install <package> without a flag installs into the current project’s node_modules folder. npm finds the project root by walking up from the current directory until it finds a folder containing package.json or node_modules, similar to how Git finds the repository root . This means npm install works from any subdirectory of the project, and the package lands in the project’s node_modules, not the subdirectory’s.
# Local install: adds to package.json dependencies.
npm install express
# Local install as a dev dependency.
npm install --save-dev jest
# Check what is installed locally.
npm list --depth=0
The package’s executables are symlinked into ./node_modules/.bin. During npm run <script>, npm prepends ./node_modules/.bin to the PATH, so a script like "test": "jest" finds the locally installed Jest binary without a global install . Outside an npm script, the binary is not on the PATH, but npx jest will find it.
b. Global installs: the system-wide CLI namespace
npm install -g <package> installs into the prefix’s lib/node_modules and symlinks the executables into {prefix}/bin . The package is not added to any package.json, because it is not a project dependency. It is a system-wide tool.
# Global install: available from any directory.
npm install -g nodemon
# List globally installed packages.
npm list -g --depth=0
# Find the global package directory.
npm root -g
The npm root -g command prints the global node_modules path, which is useful for debugging why a globally installed package is not being found. The npm list -g --depth=0 command lists only the top-level global packages, without the transitive tree, which is the readable form for an inventory of installed CLI tools .
On Unix systems, a global install may require sudo if the prefix directory is owned by root. This is a security and permission hazard: running npm with sudo can create root-owned files in the user’s home directory, and a compromised package’s install script runs with root privileges. The recommended approach is to configure npm to use a user-owned prefix, such as ~/.npm-global, and add its bin directory to the PATH. This avoids sudo entirely and keeps global installs scoped to the user.
c. npx and npm link: the alternatives to global installs
npx runs a package’s binary without a permanent global install. If the package is in the local node_modules/.bin, npx uses it. If not, npx downloads the package to a cache, runs the binary, and leaves the cache in place for future runs. This covers the “run once” case: a scaffolding tool that is used to create a project and then not needed again does not require a global install .
# Run a package without installing it globally.
npx create-react-app my-app
# Run a specific version.
npx typescript@5.3 --version
npm link connects a global install to a local project. Running npm link in a package’s directory creates a global symlink; running npm link <package-name> in a project’s directory creates a symlink from the project’s node_modules to the global package. This is the mechanism for developing a package and testing it in a consumer project without publishing, and it is also the documented workaround when a package needs to be both a global CLI and a local library .
# In the package directory: create the global link.
npm link
# In the consumer project: link to the global package.
npm link my-package
The link is a symlink, not a copy. Changes to the linked package are immediately visible in the consumer project, which is why npm link is the standard tool for local package development. The downside is that the link is not recorded in package.json, so a fresh npm install on another machine will not reproduce it.
Complete Example Session
# ============================================
# PART 1: LOCAL INSTALL (DEFAULT)
# ============================================
# Install a library that the project imports.
npm install lodash
# Result:
# - node_modules/lodash/ created.
# - package.json "dependencies" updated.
# - require('lodash') works in project code.
# ============================================
# PART 2: GLOBAL INSTALL (-g)
# ============================================
# Install a CLI tool used across projects.
npm install -g nodemon
# Result:
# - /usr/local/lib/node_modules/nodemon/ created.
# - /usr/local/bin/nodemon symlinked.
# - `nodemon` works from any directory.
# - package.json unchanged.
# ============================================
# PART 3: VERIFYING LOCATIONS
# ============================================
# Local package location.
npm root
# โ /path/to/project/node_modules
# Global package location.
npm root -g
# โ /usr/local/lib/node_modules
# Global binary location.
npm bin -g
# โ /usr/local/bin
# ============================================
# PART 4: LISTING INSTALLED PACKAGES
# ============================================
# Local top-level packages.
npm list --depth=0
# Global top-level packages.
npm list -g --depth=0
# ============================================
# PART 5: THE LOCAL CLI TRAP
# ============================================
# Install a CLI tool locally.
npm install --save-dev jest
# The binary is at ./node_modules/.bin/jest.
# `jest` alone is not on the PATH.
jest --version # command not found.
# Run it via npm script (PATH is set).
npm test # works if script is "jest".
# Run it via npx.
npx jest --version # works.
# ============================================
# PART 6: NPX AS THE GLOBAL ALTERNATIVE
# ============================================
# Run a tool without permanent global install.
npx create-react-app my-app
# Runs from cache or downloads, then executes.
# No global install, no PATH pollution.
# ============================================
# PART 7: NPM LINK FOR DUAL-USE PACKAGES
# ============================================
# In the package directory:
cd ~/my-library
npm link
# In the consumer project:
cd ~/my-app
npm link my-library
# Now require('my-library') resolves to ~/my-library.
# Changes are live; no republish needed.
# ============================================
# PART 8: PERMISSION PROBLEMS
# ============================================
# Global install without sudo on a root-owned prefix:
npm install -g nodemon
# โ EACCES: permission denied.
# Fix: use a user-owned prefix.
npm config set prefix ~/.npm-global
export PATH="$HOME/.npm-global/bin:$PATH"
npm install -g nodemon # works without sudo.
# ============================================
# PART 9: DECISION FLOW
# ============================================
# Is it imported via require()/import? โ local.
# Is it a CLI run from the shell? โ global or npx.
# Both? โ local + npx, or global + npm link.
# One-off tool? โ npx.
# Pinned per project? โ local.
# ============================================
# PART 10: THE RULE
# ============================================
# Local = project dependency. Reproducible.
# Global = system tool. Convenient, version-locked.
# npx = run without installing.
# npm link = bridge between the two.
Quick Reference
Installation Modes
| Mode | Command | Location | Recorded in package.json |
|---|---|---|---|
| Local | npm install <pkg> | ./node_modules | Yes |
| Global | npm install -g <pkg> | {prefix}/lib/node_modules | No |
Location Commands
| Command | Prints |
|---|---|
npm root | Local node_modules path |
npm root -g | Global node_modules path |
npm bin -g | Global binary directory |
npm list --depth=0 | Local top-level packages |
npm list -g --depth=0 | Global top-level packages |
When to Use Each
| Use Case | Mode |
|---|---|
| Library imported by code | Local |
| CLI tool used across projects | Global or npx |
| One-off scaffolding tool | npx |
| Package under development | npm link |
| Version pinned per project | Local |
Alternatives to Global
| Alternative | Behavior |
|---|---|
npx <pkg> | Run without permanent install |
npm link | Symlink global package into local project |
Local install + npx | CLI available via npx, no global |
Best Practices
โ Do This:
# Install libraries locally. // โ
npm install express
# Use npx for one-off CLI tools. // โ
npx create-react-app my-app
# Use a user-owned prefix for global installs. // โ
npm config set prefix ~/.npm-global
# Use npm link for package development. // โ
npm link # in package; npm link pkg in consumer.
# Verify global location before debugging. // โ
npm root -g
โ Don’t Do This:
# Install a library globally and expect require() to work. // โ
npm install -g lodash # require('lodash') fails.
# Run global installs with sudo unnecessarily. // โ
sudo npm install -g nodemon # Permission mess.
# Install every CLI tool globally. // โ
# Use npx for tools that are not used daily.
# Assume a locally installed CLI is on the PATH. // โ
jest --version # command not found; use npx.
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
require() fails for global package | Module resolution is local | Install locally |
| CLI command not found after local install | ./node_modules/.bin not on PATH | Use npx or npm script |
EACCES on global install | Prefix owned by root | Set user-owned prefix |
| Different project versions conflict | One global version | Install locally |
npm link not reproduced in CI | Link not in package.json | Install from registry in CI |
Real-World Examples
1. Local Library
npm install axios
2. Global CLI
npm install -g nodemon
3. Run Without Install
npx create-vite my-app
4. Global Package Location
npm root -g
# /usr/local/lib/node_modules
5. List Global Packages
npm list -g --depth=0
6. Local CLI via npx
npx jest --version
7. Link Package for Development
npm link # in package dir
npm link my-package # in consumer
8. User Prefix
npm config set prefix ~/.npm-global
9. Global Binary Path
npm bin -g
# /usr/local/bin
10. Local Only Check
npm list --depth=0
Visual
Two Installation Trees
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ LOCAL GLOBAL โ
โ โ
โ project/ /usr/local/ โ
โ node_modules/ lib/ โ
โ lodash/ node_modules/ โ
โ express/ nodemon/ โ
โ โ
โ require('lodash') nodemon โ
โ โ finds local โ PATH finds โ
โ /usr/local/bin โ
โ โ
โ Project-scoped. System-wide. โ
โ Version isolated. One version. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The PATH Difference
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ CLI AVAILABILITY โ
โ โ
โ Global install: โ
โ binary โ /usr/local/bin/ โ
โ /usr/local/bin is on PATH. โ
โ `nodemon` works anywhere. โ
โ โ
โ Local install: โ
โ binary โ ./node_modules/.bin/ โ
โ .bin is on PATH only during npm scripts. โ
โ `jest` alone โ command not found. โ
โ `npx jest` โ works. โ
โ `npm test` โ works if script is "jest". โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Decision Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ LOCAL OR GLOBAL? โ
โ โ
โ Does code import it? โ
โ โโโ Yes โ LOCAL โ
โ โโโ No โ Is it a CLI tool? โ
โ โโโ Yes โ Used daily? โ
โ โ โโโ Yes โ GLOBAL โ
โ โ โโโ No โ npx โ
โ โโโ No โ LOCAL โ
โ โ
โ Both? โ LOCAL + npx, or GLOBAL + npm link. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
npm link Bridge
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ NPM LINK โ
โ โ
โ ~/my-library (package) โ
โ โ โ
โ โ npm link โ
โ โผ โ
โ /usr/local/lib/node_modules/my-library โ
โ โฒ โ
โ โ npm link my-library โ
โ โ โ
โ ~/my-app/node_modules/my-library (symlink) โ
โ โ
โ require('my-library') โ ~/my-library โ
โ Changes are live. No republish. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Local install | Default, ./node_modules, in package.json |
| Global install | -g, {prefix}/lib/node_modules, not in package.json |
| Global binary | {prefix}/bin, on PATH |
| Local binary | ./node_modules/.bin, on PATH during npm scripts |
npm root -g | Prints global node_modules path |
npm list -g --depth=0 | Lists global top-level packages |
npx | Run without permanent install |
npm link | Symlink global package into local project |
| Unix prefix | /usr/local by default |
| Windows prefix | %AppData%\npm |
Key takeaways:
- Local installs are for libraries; global installs are for CLI tools. Node’s module resolution searches the local
node_modules, not the global one. A globally installed library is invisible torequire(). - The global install’s primary effect is the
PATHsymlink. The package goes to{prefix}/lib/node_modules, and its binary goes to{prefix}/bin, which is on the shell’sPATH. This is why a global CLI works from any directory. - A local CLI is not on the
PATH. Its binary is in./node_modules/.bin, which npm adds to thePATHonly during script execution. Usenpxto run it outside a script . - Global installs create cross-project version conflicts. One global version cannot satisfy projects that need different versions. Local installs isolate versions per project .
npxreplaces most global installs. It runs a package without permanent installation, which covers one-off tools and scaffolding utilities .npm linkbridges a global package into a local project. It is the documented workaround for packages that are both a library and a CLI, and the standard tool for developing a package against a consumer project .sudois a symptom, not a solution. A global install that requiressudomeans the prefix is root-owned. Configure a user-owned prefix instead.
Remember: The -g flag is a location selector, not a quality selector. It installs to the system-wide node_modules and symlinks the binary to the PATH. That is correct for a CLI tool you run from any directory, and wrong for a library your code imports. The default local install puts the package in the project’s node_modules, records it in package.json, and isolates its version from every other project. The Node.js documentation recommends local installs for everything except CLI tools, and npx covers most of the remaining cases . When a package is both, npm link avoids installing it twice.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!