How to avoid installing Node.js packages globally and use node_modules instead

It is a very common practice to install NPM packages globally. It's an easy method to install and run executable programs that helps with your development. However, there are some pitfalls to it, particularly when it comes to distributing your application working in a team.

Reasons to not install NPM packages globally

Requires to install them system-wide

Global install leads to your project dependency installed system-wide into a directory such as /usr/local/lib/node_modules. It's good practice to not trust random packages on the internet with access to system directories. Therefore, it's best to keep the package access and versioning to specific projects and within ./node_modules of the project directory.

Extra steps to install required dependencies

For every project, you would run npm install or yarn to install all the required dependencies. It's the convention that is understood by all Javascript and Node.js developers. Using scripts that require global dependencies install separately requires additional steps.

$ npm install -g typescript
$ npm install -g nodemon
$ npm install -g mocha
# And so on...

It then required additional documentation of what additional dependencies to install. Possibly worst, is that you can forget and miss out on writing documentation. Thus, leading to scenarios such as scripts in the package.json not working since the additional dependencies are not installed.

"scripts": {
  "dev": "nodemon bin/www",
  "test": "mocha",
  "transpile": "tsc myfile.ts"
},

Help! Why does it say tsc: command not found when I try to run the script? What am I suppose to do?

  • Do I need to run npm install -g tsc ?
  • Or is it npm install -g typescript ?

Versioning is not consistent

When installing them globally, the would be presumably install the latest version. Depending on when you or your team members run the command, you can end-up installing different versions of the package causing breaking changes or different behaviors. You may not even be able to keep track of the version that worked if you need to revert.

When to install them globally (Exceptions)

You can install packages globally when you are using a CLI tools that you do not expect to have to distribute. That means packages fit neither in the dependencies or the devDependencies section in package.json file.

For example, I use npm-check-updates to keep packages in a project up to date. It is not a dependency for the code to run and it is not a tool that necessarily needs to be kept aligned and consistent for everybody. There are many other and preferred methods of managing package versions. Therefore, it does not need to be installed and kept at a consistent version for everybody.

Steps to using local package installation

Step 1: Install packages locally

Instead of installing packages globall such as the commands above, install the packages locally and set the dependency scope appropriately.

$ npm install typescript --save-dev
$ npm install nodemon --save-dev
$ npm install mocha --save-dev

This method installs the dependencies and save them to the package.json file as a devDependencies as these are not required to be install in production with npm install --production, or the now preferred npm install --omit=dev.

Step 2: Update package.json to use the local package installation

Using npx (simplest)

Just add npx to trigger the package runner. This command would execute the locally installed and automatically linked executable.

"scripts": {
  "dev": "npx nodemon bin/www",
  "test": "npx mocha",
  "transpile": "npx tsc myfile.ts"
},

Executable path (alternative)

Alternatively, if you want to be fancy and do it the old fashioned way, you can specify the executable path directly. This method was from before the handyness npx came in to newer NPM versions. The executable path would typically be in the ./node_modules/[package_name]/bin directory.

"scripts": {
  "dev": "./node_modules/nodemon/bin/nodemon.js bin/www",
  "test": "./node_modules/mocha/bin/mocha.js",
  "transpile": "./node_modules/typescript/bin/tsc myfile.ts"
},

Note: There's hardly any advantage to this method over using npx

Conclusion

The 3 reasons use the locally install NPM package dependencies are:

  1. Prevent extra steps to install dependencies
  2. Manage consistent package versions
  3. Keep them scoped to a project instead of system-wide

Update the scripts in package.json to use npx and ensure to have the packages installed and listed in the dependencies list.

Wei-Ming Thor

I write guides on Software Engineering, Data Science, and Machine Learning.

Background

Full-stack engineer who builds web and mobile apps. Now, exploring Machine Learning and Data Engineering.

Writing unmaintainable code since 2010.

Skill/languages

Best: JavaScript, Python
Others: Android, iOS, C, React Native, Ruby, PHP

Work

Engineering Manager

Location

Kuala Lumpur, Malaysia

Open Source
Support

Turn coffee into coding guides. Buy me coffee