Don't `npm install` your way to success: a different way of thinking about dependencies

avatar
Ryan Block
March 22, 2022

packages Photo by Claudio Schwarz on Unsplash

It is sometimes said that your greatest strength is also your greatest weakness, and it is certainly so that JavaScript’s greatest strength is its incomprehensibly massive developer ecosystem. There’s a JS module for just about everything imaginable, and thousands of new modules and module versions are published daily.

The intention of this post is to demonstrate a different approach to adopting and using dependencies. And while we’ll largely refer to JavaScript, these principles likely apply to numerous other ecosystems as well.

Why we use dependencies

As developers, it’s easy to see why we rely on so many battle-tested dependencies. After all, doing so seems to conform with many key principles of software development:

Adopting dependencies has become a way of life for most of us in the JS ecosystem. At Begin, we often refer to it as npm install-ing your way to success.

What’s wrong with this picture

The unfortunate reality that’s often forgotten when typing in npm i is that dependencies only create the illusion of trustworthiness, safety, and efficiency. Unless you personally audit the code in your dependencies – and the code in your dependencies’ dependencies, and so on – you are effectively outsourcing the safety and security of your application to “enough eyeballs”, e.g. “the wisdom of the crowds”.

This is also known as bandwagon fallacy, or the appeal to popularity, and you should be on guard when you see it in action. The number of developers who’ve adopted a dependency has very little to do with that module’s safety and security. In fact, it’s inverse: the more popular the package, the more useful it is as a vector for delivering malicious payload.

All code is a liability

As developers, we love harnessing the power of code. Finding ways to make the lump of rock we tricked into thinking do precisely what we imagine is a surefire endorphin hit every single time. But it’s also important to remember that all code is a liability, and the more distant you are from the code being executed in your app – as is the case in sub-sub-sub-dependencies – the greater the liability of that code.

I refer to this concept as total code liability. The thinking goes something like this:

Fortunately, most developers already view code as a liability (albeit not necessarily in those terms). Many of the most common software development principles, like KISS, DRY, and YAGNI, are fellow travelers with the general belief that code is a liability.

At Begin we routinely see folks with node_modules directories with hundreds of MB of dependencies – literally millions, if not tens of millions of lines of code. Why are we so quick to trust our app’s quality, security, and speed to millions of lines of code from unknown authors coming in as dependencies of dependencies?

If you’re not a little scared by this idea, you should be. Last week a maintainer of node-ipc released a protestware version of the package to corrupt files on target users hard drives. leftpad, ua-parser-js, coa, rc, colors, faker, and the dozens of other hacked, vandalized, or malicious packages that have made their way into production software are only the beginning of the supply chain attacks to come. Tools like Snyk may help us mitigate such problems, but they are also purely reactive, meaning responses come after an incident may have occurred. What if we do the work to minimize the chances of an incident ever happening to begin with?

Breaking the addiction

The message here is not to stop relying on dependencies. Instead, I propose that as authors and maintainers of code – and especially as consumers of dependencies – we must instead endeavor to reduce our total code liability. We should be discerning with every single npm i, and that process begins with acknowledging that:

All this be made actional by following two simple steps the next time you find yourself typing npm i:

1. Actively try not to npm i

There will be a moment where you’re working through a problem and arrive at a task that is perhaps best suited to [your favorite dependency for solving that task]. This is the moment to consider the total code liability of your application.

I suggest slowing down for a moment, assessing the problem, and asking the following questions:

This is only slightly more work than npm i, and applied throughout the entire dependency tree, you’ll soon notice that simply taking these factors into consideration often results in a completely dramatic reduction in external dependencies.

Example

uuid is a great package for creating random UUID’s but as of Node.js v14.17.0 it’s unnecessary as the built-in crypto module contains a randomUUID method which generates a random RFC 4122 version 4 UUID, the same as the uuid package.

2. When you need to install a dependency, apply a bit of diligence

This one seems painfully obvious to say out loud, but it’s super important and bears noting: don’t just pick the popular dependency for a task. In fact, bias against the popular dependencies, as popularity usually comes with feature requests (read: bloat, which increases your total code liability), and said popularity makes it a juicier target for a supply chain attack.

If you need to install an external dependency, ask the following questions:

In my experience, with a minimal amount of digging, there’s almost always a smaller, leaner, zero-dependency module out there waiting to be found.

Example

Both dompurify and xss are excellent packages for sanitizing input but dompurify and it’s dependencies weigh in at 10 Mb while xss and its dependencies are only 241 kb. For a deeper dive read our post on Sanitizing Input.

Wrap-up

I hope we’ve inspired you to reconsider the practice of npm install-ing your way to success, to be a bit more skeptical of adding dependencies – especially the popular ones – and to consider the concept of code as a liability. Put into practice, we believe this approach can help you to create safer, more secure, more efficient software without sacrificing developer experience or developer velocity.


Bonus: inspect modules in isolation

Here’s a fun, quick hack to look at any npm module in complete isolation, and to gauge its size. Add this to your .bash_[profile|rc]:


npmtest () { mkdir ~/YOUR_MODULE_TEST_PATH && mkdir ~/YOUR_MODULE_TEST_PATH/$1-test/ && cd ~/YOUR_MODULE_TEST_PATH/$1-test/ && echo {} > package.json && npm install $1 && du -sh node_modules/*; }

Then run: npmtest gatsby