Local vs global npm dependencies
May 04, 2020
Working on Node.js based applications usually requires you to install dependencies that are needed to provide functionality within the project, either for development or as part of the project logic at runtime. A number of the development dependencies provide a command line interface for interacting with the package and making use of the functionality for things like scaffolding, compiling, bundling, publishing, etc.
For these CLI-based dependencies, the author usually recommends installing the packages globally, for ease of use. However there are benefits to installing these dependencies locally instead of globally, which might not be immediately obvious.
All project dependencies declared in a single place
Whenever you install local dependencies, they are listed in the package.json file which acts as a manifest showing all the information about a package, including the dependencies it needs. This means if you install your global dependencies locally, it becomes obvious what all the dependencies of the project are.
For instance, assuming you have a project that uses typescript, you can install the typescript CLI package globally (yarn global add typescript
) and call tsc path/to/project
whenever you want to compile the code. Now you might even add a script in the package.json file for this, so it’s more obvious to you and any other people working on the project what command to run to compile.
"scripts": {
"compile": "tsc ."
}
However imagine you decide to work on this project on another computer, or some other person tries to work on the same project in their own machine. After installing the dependencies (yarn install
), running the compile script would fail on the first attempt because yarn can’t find the tsc
command. You then need to install typescript globally on that machine as well to continue working on the project. Now imagine typescript now has several major updates and now the latest version is not backward compatible with the version of typescript used in the project initially. You would get another error and would have to start troubleshooting to figure out what exact version of typescript you need to install to get this working.
On the other hand, installing the typescript
package as a local dependency (yarn add --dev typescript
) would mean, any one that runs yarn install
in that project would also install typescript automatically (and the right version as well), and the compile script would just work as expected.
Multiple projects with different versions on same machine
As a developer working on Node.js based projects, you likely have multiple projects on your local machine that you work on at different times. Assuming several of these projects use typescript, and you install typescript globally. You worked on project X a couple of months back when typescript was at version 2.7.1. You haven’t worked on it in a while.
You get started working on another project Y that uses the latest stack features (react hooks, latest typescript esnext features, bells and whistles). Project Y needs at least [email protected] for it to compile properly. You try compiling the project and you get an error because typescript doesn’t understand the nullish coalescing operator. Now you’re wondering why this project worked in your colleague’s machine but isn’t working on yours. After a couple of hours debugging, you realize your global typescript version is at 2.7.1. You decide to update the global typescript package. Project Y compiles! That’s great news.
Things move along quickly and now you have some tasks to implement in Project X. You open it up in your editor, make the changes and compile. Now there are several typescript linting errors that weren’t there before. You only changed 2 lines of code but typescript isn’t compiling because of errors in an entirely different file you didn’t touch. After several hours of debugging, you realize that you are using [email protected] but the project was built with [email protected]. You now have two choices: forcefully fix the code and update it to use the latest version of typescript, or you downgrade typescript to 2.7.1. You quickly figure out that none of these options is good enough 😞
Imagine the same scenario but you are using a locally installed typescript dependency in both Project X and Project Y. You wouldn’t have any of the issue above because each project would have its own version of typescript to work with, without affecting the other project.
Using local dependencies from CLI
One of the advantages you would usually see with global dependencies over local dependencies is that: you can just call the command of a global dependency from the command line, but you can’t do the same with local dependencies. Using the previous example of typescript. If you installed typescript globally, you can just run the following from anywhere and it would work:
tsc /path/to/project
You can’t do the same with local dependencies. However if you manage your dependencies with yarn, you can run a local command in a similar way with yarn. The equivalent of that command would be:
yarn tsc .
However, you need to run this command from within the project working directory for yarn to be able to find the local dependency to run.
In my personal opinion, this is a relatively small issue that can be worked around compared to the benefits of local dependencies.
You also have the option of adding the command as a script in the package.json file, in which case you can run it with both yarn and npm.
"scripts": {
"compile": "tsc"
}
You can run it using yarn compile
or npm run compile
. This would execute the tsc
command from the local dependencies of the project, if available.
When to use global dependencies
While I generally recommend using local dependencies, I have to say that there are some cases where you need global dependencies.
Scaffolding the project
When you need a package to scaffold a project, you don’t have a project yet and so no package.json file to have local dependencies in. In such cases, you need to install the packages as a global dependency. Examples of these are: angular CLI for scaffolding angular projects (ng new project-name
), create-react-app for scaffolding react projects (create-react-app project-name
), vue CLI for scaffolding Vue.js applications (vue create project-name
), and the many others. These commands create the project directory as well as creating the package.json file and installing the dependencies for you. Usually they would install the dependencies locally as well for scaffolding other part of the project (like components, routing, styles, etc) so you don’t depend on the global dependency for that.
Running task-specific, project-agnostic commands
There are a number of dependencies that are used to perform tasks that are not specific to any project. These commands are similar in function to the other commands you have available to you in the command line. They provide utilities that can be used in a wide range of cases. For example, starting up a simple local server with serve
, getting help about other commands with tldr
, and many others. These commands provide general utility functionality and as such are better off installed globally.
Bonus tip: using npx
or yarn dlx
for one-time global packages
Like I mentioned above, there are times when you need to install packages globally. However, there are some packages that you don’t need to keep installed always, especially if you just need to run the command once as opposed to using it frequently. Examples of such packages would be the scaffolding packages and some others (like starting up a local server). Depending on the kind of tasks you work on, these are not packages you might need to have installed on your machine always. More often than not, you just scaffold a project one time and proceed with developing the project without needing the scaffolding functionality afterwards.
For these cases, you can make use of the npx
command (provided by npm), or the yarn dlx
command (available in yarn v2, not yarn v1). For example if you wanted to scaffold a react app, you can run npx create-react-app project-name
or yarn dlx create-react-app project-name
and this would temporarily download the create-react-app
dependency and execute the command to scaffold the project, without installing the package globally in your machine.
Conclusion
Preferring locally installed dependencies over global dependencies provide a number of benefits that would be helpful as the number of projects and complexity of the projects increases. Also consider executing the dependencies without installing them using npx
or yarn dlx
.
Did you find this useful? Do you think there are better approaches to take, or questions? You can reach out to me on twitter @imolorhe.
Write about what you learn. It pushes you to understand topics better.
Sometimes the gaps in your knowledge only become clear when you try explaining things to others. It's OK if no one reads what you write. You get a lot out of just doing it for you.
@addyosmani