How the Heck Do You Install npm Packages?

Avatar of Josh Collinsworth
Josh Collinsworth on

By now, you’re becoming quite knowledgeable with npm! So far, we’ve broken down the three letters in “npm” to gain a better understand of Node and package managers. In the previous chapter, we even installed Node and npm while getting acquainted with Node Version Manager, or nvm. Next up in this beginner’s guide to npm is likely why you’re here in the first place: installing npm packages.

Guide chapters

  1. Who the Heck is This Guide For?
  2. What the Heck Does “npm” Mean?
  3. What the Heck is the Command Line?
  4. What the Heck is Node?
  5. What the Heck is a Package Manager?
  6. How the Heck Do You Install npm?
  7. How the Heck Do You Install npm Packages? (You are here!)
  8. What the Heck Are npm Commands?
  9. How the Heck Do You Install an Existing npm Project?

A quick example

We can install our very first package with the npm install command (or npm i for short), followed by the name of the packages we want to add to our project. For example, the Node package for Sass is simply called “sass” which means we can add to a project like this (just be sure you’re in a new folder you created for this little project first):

npm install sass

That’s all you need! Type that and npm goes straight to work:

Screenshot of a dark terminal window with a project called nom-test. The first command is npm install which adds 17 total npm packages with zero vulnerabilities.

What’s happening behind the scenes there is that npm tries to find a package named sass in the npm package registry. If it finds that package (which it does), npm installs it to the project in an automatically generated node_modules folder (more on this in a bit) located in the project root folder, including everything the package needs to run. (This is why you see that npm added 16 packages and audited a total of 17 npm packages, instead of the Sass package alone—it, too, has dependencies!)

Once we’ve run the install command, you may notice that you do not see anything named “sass” in the project folder as you might expect. Oddly, however, we do see a three new items in the project folder: two JSON files named package.json and package-lock.json, plus one entirely new node_modules folder.

Screenshot of the VS Code editor with a package.json file open. The file contains the project name, npm-test, and includes a dependencies section that contains the Sass npm package.

What are these!? We asked npm to install Sass, not all this stuff. That’s not part of Sass… right? Well, that’s correct, but there’s a very good explanation why those new items were generated in the project folder. Let’s look at what just happened.

What happens when you install a package

When you install (or uninstall, or update) a package, npm does most, if not all, of the following four things:

  1. Updates the package.json file in your project, if needed;
  2. updates the package-lock.json file (called the “lockfile”) that contains all of the technical specifics;
  3. installs the actual package files—and any other packages the original package might depend on (inside of the node_modules folder); and
  4. runs an audit of the installed packages.

Let’s step through those one-by-one.

package.json and package-lock.json

These two JSON files work together to ensure an accurate record of all the dependencies in your project (and all of their dependencies, and all of their dependencies’ dependencies, and so on). The difference is a little technical, but loosely explained: the lockfile is the in-depth, precise snapshot of the project’s dependency tree, and package.json is a high level overview, which can also contain other things. The main packages you install may be listed in package.json, but package-lock.json is where the entire dependency tree is tracked.

The lockfile is also never supposed to be updated by hand; only by npm. So be sure to avoid mistaking the lockfile with the package.json file.

When you share or collaborate with others on a project, npm knows where the project came from and exactly what you have installed in the project by these two files. It can replicate that environment precisely on anyone else’s machine, thanks to their info. Both files are meant to be committed to your Git repo, and serve as your project’s dependency blueprint. That way, when another developer on your team clones the repo and runs the npm install command, npm knows exactly which packages to install, keeping you and your colleague in sync.

If you open package.json, you won’t see much, but it’s worth a peek just to see what’s happening:

{
  "dependencies": {
    "sass": "^1.43.4"
  }
}

You probably won’t see that exact version number (since the package has been updated since the time of writing), but you should see sass listed inside a JSON dependencies object. The number itself (1.43.4 in this case) indicates the specific version of Sass that is installed.

As a brief but important side tangent: the carat character (^) at the beginning of the version number lets npm know that it is allowed to install minor updates to the package. In other words, it tells npm that the installed Sass package must be at least version 1.43.4, but can be any higher 1.x.x version, as long as it’s still under 2.0.0. npm generally chooses the latest stable version when a package is installed, but adds this to allow for non-breaking updates. That bit is called “semantic versioning” and it’s a blog post unto itself, but not unique to npm.

Anyway, that covers the two JSON files. Let’s talk about the node_modules folder next.

node_modules

node_modules is where all the actual package code lives; it’s where your installed Node packages and all the stuff that makes them run actually get installed. If you open up the folder right now as you’re following along, you’ll find a sass folder, but alongside several other folders as well.

The reason for the additional folders is that when you install a package, it may need other packages to run properly (as Sass clearly does). So, npm automatically does the hard work of finding and installing all of those dependencies as well. As you may have guessed, those dependencies may also have other dependencies of their own, and so the process repeats, so on and so forth, until we’ve finished crawling the dependency tree to its furthest branches and absolutely everything we need is installed (or until we’ve hit an error of some kind, though hopefully not).

For this reason, it’s common for a project to have node_modules subfolders in the hundreds or more, which add up quickly in terms of disk space. node_modules can often get pretty hefty.

If you’re wondering how you would commit a super large folder like node_modules to a project’s repository, here’s an important note: Unlike the JSON files, the node_modules folder is not meant to be committed to Git, or even shared. In fact, just about every example of a .gitignore file (the file that tells which files Git should skip when tracking files) includes node_modules to ensure Git never touches or tracks it.

So, how does anyone else on your team get those packages? They run npm install (or npm i for short) from the command line to download the dependencies directly from the source. This way, there’s no need to commit (or pull) massive amounts of data to and from the origin repo.

Using caution when installing dependencies

This massive web of dependencies and their great-great-grand-dependencies can lead to situations where a small utility library of some kind that provides a useful service can become adopted by many other packages, which are, in turn, used in many other packages, until eventually the original code winds up quietly installed on a significant percentage of sites and apps.

It might sound wild (if not downright scary) that, in the process of installing your one package, you may actually be letting a whole bunch of other stuff through the door. It can feel like inviting a new friend to your house party, who then shows up with 20 uninvited strangers. But it’s not as weird or scary as it may seem, for a few reasons:

  1. Most npm packages are open source. You and anybody else can easily peek under the hood and see exactly what the package is doing. You can also look the package up on the registry (npmjs.com) to see how many times it’s been installed, when it was last updated, and other relevant info. If a package is fairly popular, you can be reasonably certain it’s safe.
  2. There’s a vast world of functionality that many projects will need. Consider date formatting, handling HTTP requests and responses, throttling, debouncing, or animations, just as quick examples. It doesn’t make sense to keep reinventing the wheel and hand-coding these things every time they’re used in a new project.
  3. Installing a package isn’t really that different than installing an app on your phone, or a plugin on a WordPress site. The difference is that we don’t get the same glimpse into the inner workings of those apps and plugins the way we do with packages, nor what other things those apps and plugins might rely on. Odds are good they pull in many smaller packages, too, in some way or another.

A degree of caution is a good idea in any environment in which one can install and execute arbitrary code, of course. Don’t get me wrong. I’d be lying if I said bad actors have never successfully taken advantage of this system. But know that there are many processes in place to keep things from going awry. When in doubt, stick with the most popular packages and you’ll be fine.

Also know that npm runs automatic security audits for you, which brings us to the final point in this section.

What is npm audit?

When we installed sass earlier, we saw the following message in the terminal once it finished:

found 0 vulnerabilities

However, you may see some warnings instead—like this old project of mine in the following image. I decided to boot it up and run npm install (npm i) after it’s sat for at least a couple of years. Let’s see how it did:

Screenshot of an open terminal window showing the process of installing npm packages with the npm i command. 212 npm packages are installed but the terminal shows there are 93 vulnerabilities, where 46 are moderate, 42 are high, and 5 are critical.
YIKES!

Packages with known vulnerabilities are called out by npm audit, which runs automatically any time you install a package. If you see a message like this, don’t be too alarmed; many vulnerabilities, especially in the “moderate” category, carry very low real-world risk, and may only be relevant in highly specific situations. (For example, it may only be one method in a package, when used in a particular way, that makes it vulnerable.)

Still, it’s best to address what we can, which is what the npm audit fix command is for. Adding fix to the end tells npm to go ahead and update to a new minor version of any package with a known vulnerability of some kind. The “minor version” part is important; minor versions aren’t supposed to contain breaking changes, only updates. That means it should be safe to run an update this way without any risk of breaking your project.

If bumping the package up by a minor version number doesn’t do the trick, you can add the --force flag to the original command:

npm audit fix --force

This is a risky maneuver, however. Giving npm permission to “use the force” means it can now install major version updates to address vulnerabilities—which means it may make breaking changes or introduce incompatibilities. I wouldn’t recommend doing this unless there are critical vulnerabilities that npm audit fix is unable address and you are willing and able to spend significant time afterwards troubleshooting, if necessary.

One last note on this topic: it helps to know that you can sometimes fix some unexpected issues with npm projects by deleting node_modules, and re-running npm install. That’s the npm way of “turning things off and on again,” which is something I’ve done many, many times myself.

What’s next

Now that we’ve thoroughly explored the rabbit hole of how npm works under the hood, let’s get back to actually doing things, shall we?