Why You Shouldn’t “npm install -g”

I’ve been doing some reading lately on new (to me) tools in the Node.js ecosystem. This ecosystem is certainly vibrant, with lots of interesting things going on all the time, but I’m concerned about a pattern that I see popping up when people write about it.

It’s an old pattern—one I’ve seen many times in many different contexts over my decades of working on Unix-like systems—but it seems even more common now that OS X is the development platform of choice. The pattern’s telltale sign: npm install -g.

I last wrote about this pattern in 2014, just before I became an Atom, as I was working through the Ruby on Rails tutorial. As I went through that otherwise solid documentation, I was unsettled by the number of times I was asked to invoke an installation command prefixed with sudo.

Don’t get me wrong; Sudo is a great piece of software. It helps narrow down the scope of tasks performed as the superuser. Sudo definitely has its place. But too often, it’s used to change global state on the operating system when local state would suffice. My friend Trenton Broughton once put it thus:

“Sudo install is like using global variables to manage state.”

And though npm install -g, run as your user account with no root privileges, is safer than using sudo to install all the things, it’s still better to invoke it as rarely as you can.

Global Installs Affect Everything on Your Computer

Even though most of our Unix-like systems are no longer used by multiple users, they are still used for a wide variety of software.

When you put something into a directory like /usr/local/bin, it’s certainly convenient for you; the software becomes immediately available no matter where you are on the system with no further action on your part.

But while it’s easy for you to use that software anywhere, it also becomes easy for other software elsewhere on your system—even software that’s very locally scoped—to pick up on the software you’ve installed. This can cause problems that later become very hard to track down, or even trace to their original cause.

Keep your /usr/local/bin populated with tools that are globally useful for everything on your system.

Projects Can Use Different Versions of Things

In an ecosystem as fast-moving as Node.js, tools change every week. npm is good at locking down our code dependencies so we can address that churn when we need to, but if you’re doing more than one thing on your computer (which I’d argue most people are!), using globally-installed tools means all projects will see whatever version you’ve got globally installed.

npm has already solved this problem. For build tools that aren’t part of your code dependencies, you can use something like this:

npm install --save-dev tool

A tool installed like this will install its command-line interface into node_modules/.bin, and you can invoke it from there. You can also use a handy shell function like this in your .profile:

function nrun {
        $(npm bin)/$@

Now, running your tool is as simple as typing nrun tool—no matter what project you’re in or how deep you’re into it, npm will find the correct directory for command-line tools and execute the one that goes with your project.

Things Can Use Different Versions of Node

That global tool you installed might (and, I would argue, absolutely should) lead off with the env shebang, #!/usr/bin/env node. This convenient header finds the first instance of the node binary on your PATH and uses it to execute your tool.

If you’re doing multiple Node.js projects on your system, you may have multiple versions of Node.js installed. (I certainly do.) If you change your PATH to use these different versions of the node binary while you work, tools you invoke will use whatever binary comes first on your PATH. If you’re using project-local tools, these will be a matched set; if you’re using global tools, they won’t.

Local Installs Are Better for Sharing Code

Most of the npm install -g invocations I’ve seen are for installing tools that are really only needed for the project you’re working on. Not only is this risky for reasons I’ve outlined above, but it also sets up an undeclared dependency—and other developers using your code will curse you for it.

Using npm install --save-dev as above will record your package into packages.json, leaving developers who work with your code a mere npm install away from having everything they need.

You can simplify other developers’ lives further by adding ready-made aliases for any tools that developers will be using frequently to the scripts section of packages.json:

"scripts": {
    "bower": "bower",
    "gulp": "gulp",
    "serve": "gulp serve"

With this, they don’t even need a utility like nrun above in play to easily run tools. Everything is just an npm run away (e.g. npm run gulp build or npm run serve).

Some Things Should Still Be Installed Outside Projects

None of this is to say you should not be installing Node.js tools exclusively inside project directories. Such a workflow, even if it’s possible, isn’t likely to run very smoothly. Some tools, due to their position in the toolchain below your project’s own scaffolding, are best installed globally on your system (or at least in your home directory’s own bin folder). These are mine:

  1. The Node Version Manager. Having the nvm command available globally makes it possible to manage and switch between different Node.js versions for different projects.
  2. avn and avn-nvm. These tools facilitate easy switching to appropriate nvm-managed versions of Node.js automatically when I move into a project.
  3. “System” versions of Node.js and npm. These aren’t necessarily (and should not be) the version of Node.js and npm I use with all my projects; I choose that on a project-by-project basis. But these versions are useful for bootstrapping, running tools like avn that run under the level of my projects, or just quickly bashing out some JavaScript in a REPL.
  4. System tools that happen to be written in Node.js. My go-to example here is Keybase. Keybase is written in Node.js, but it isn’t a project dependency—it’s a tool that’s not applicable at all to my projects. Therefore, it makes perfect sense to install it in /usr/local/bin.

Note that there isn’t a Gulp in that list, a Bower, a Browserify, or anything else—those come into play when a project needs them.

When any project I’m working on needs a tool, I can rest assured that tool is always just an npm install away. No muss, no fuss, and on we go.