| |

npm 08 ๐ŸŒ Global vs Local Package Installation (-g flag)

โฑ๏ธ Estimated read: 15 min | |

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

ModeCommandLocationRecorded in package.json
Localnpm install <pkg>./node_modulesYes
Globalnpm install -g <pkg>{prefix}/lib/node_modulesNo

Location Commands

CommandPrints
npm rootLocal node_modules path
npm root -gGlobal node_modules path
npm bin -gGlobal binary directory
npm list --depth=0Local top-level packages
npm list -g --depth=0Global top-level packages

When to Use Each

Use CaseMode
Library imported by codeLocal
CLI tool used across projectsGlobal or npx
One-off scaffolding toolnpx
Package under developmentnpm link
Version pinned per projectLocal

Alternatives to Global

AlternativeBehavior
npx <pkg>Run without permanent install
npm linkSymlink global package into local project
Local install + npxCLI 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

PitfallWhy It HappensFix
require() fails for global packageModule resolution is localInstall locally
CLI command not found after local install./node_modules/.bin not on PATHUse npx or npm script
EACCES on global installPrefix owned by rootSet user-owned prefix
Different project versions conflictOne global versionInstall locally
npm link not reproduced in CILink not in package.jsonInstall 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

ItemValue
Local installDefault, ./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 -gPrints global node_modules path
npm list -g --depth=0Lists global top-level packages
npxRun without permanent install
npm linkSymlink 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 to require() .
  • The global install’s primary effect is the PATH symlink. The package goes to {prefix}/lib/node_modules, and its binary goes to {prefix}/bin, which is on the shell’s PATH. 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 the PATH only during script execution. Use npx to 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 .
  • npx replaces most global installs. It runs a package without permanent installation, which covers one-off tools and scaffolding utilities .
  • npm link bridges 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 .
  • sudo is a symptom, not a solution. A global install that requires sudo means 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!