4 Ways to Minimize Your Dependencies in Node.js
Tags: node
Published: 2022-09-14 11:29:46

We all know the joke about how `node_modules` is the heaviest object in the universe.

For example, a project that uses only `fastify`, `knex`, `typescript`, and `uuid` generates an 83MB `node_modules` folder! That's huge! And those four packages are far from a complete set for a relatively small back-end. A more realistic size for `node_modules` is north of 100MB, in some cases reaching 1GB.

In this post, we'll explore four methods to minimize your code dependencies, resulting in faster CI/CD execution and safer code.

But first, let's touch on some of the problems with heavy `node_modules`.

## The Issues with Heavy `node_modules` in Node.js

Heavy `node_modules` can cause slower CI/CD pipelines, as dependencies are usually installed during those pipelines, and require network calls to a registry (whether `npm` or your mirror of it). This affects your development experience.

Moreover, package creep can introduce serious issues like security vulnerabilities, as you don't own the code that resides inside the packages.

Let's jump into how to minimize the dependencies in your Node.js code.

## Method One: Check the Age of Node.js Packages

Imagine a situation where you have a function that looks like this:

```typescript
function findSomethingByIds(ids: number | number[]) {
  ...
}
```

This function's purpose is to either find a single entity or a list of entities by their id. It's common practice with different repository patterns.

However, let's say you want to know whether you've requested a single entity or a list of entities. A quick npm search leads us to [npm/isarray](https://www.npmjs.com/package/isarray), an insanely popular package with over 62 million weekly downloads! You know the drill:

```shell
npm i isarray
```

Don't forget:

```shell
npm i -D @types/isarray
```

But wait! Before you do that, have you noticed that this package is **three years old**? And if you do a better search, you will discover that `isArray` is now part of the JavaScript core, can be invoked using `Array.isArray`, and is perfectly supported in Node.js version 10 and up.

[Read more about `Array.isArray` in Mozilla MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray).

Package age is a good first indicator. Many old (2+ years) packages might have security vulnerabilities or be outdated. Some of the outdated packages are also merged into the official JavaScript spec.

Another example is the [trim](https://www.npmjs.com/package/trim) package. It has more than 4 million downloads, and while it's not that old (only 1 year), a native solution exists in JavaScript core: [String.prototype.trim()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim).

So always try to find a native solution first, as JavaScript evolves quickly and its standard library is always expanding. Don't be fooled by the _weekly downloads_ counter in npm.

## Method Two: Using 'One-liner' Node.js Packages

Many 'simple' packages are actually what I call 'one-liners'. A one-liner is a package that contains very few lines of code.

If we continue with our `isArray` example from above, by examining the content of `index.js` we can see that it's a simple one-line function:

```javascript
var toString = {}.toString;

module.exports =
  Array.isArray ||
  function (arr) {
    return toString.call(arr) === "[object Array]";
  };
```

_[Source: GitHub](https://github.com/juliangruber/isarray/blob/master/index.js)_

I always recommend you look at the source code of packages, at least simple ones because they can teach you a lot. Looking at the above source code, we discover one important thing: that `Array.isArray` exists (the same conclusion we came to with method one).

However, even if we did not have a native is-array method, the entire function is a simple if statement. And instead of requiring this package as a dependency, we can simply write the code as part of our project. It's worth stopping for a minute and discussing the pros and cons of such an approach.

### Using Npm vs. Writing Your Own Code

Npm is a public registry, and the code contributed to npm comes from people worldwide. It is amazing that we have such a big repository of free and open-source code. However, this also comes with some disadvantages.

Npm, unfortunately, is known to be at high risk of attack. Packages might get compromised, whether by third parties or by developers themselves.

We all remember the scandal around [`left-pad`](https://qz.com/646467/how-one-programmer-broke-the-internet-by-deleting-a-tiny-piece-of-code/) and the recent rise in crypto-mining and crypto-stealing code that resides inside popular npm packages.

While it's impossible to audit every single package we install, we can lower the attack vector by using fewer packages in our projects. A great way to reduce the number of packages we depend on is to write trivial packages ourselves.

On the other hand, by writing some code ourselves, we deprive ourselves of the community's sheer knowledge. Multiple people maintain popular packages, so they can quickly react to new vulnerabilities (for example, by monitoring the GitHub issues page of their package). This is something you, as a sole developer or part of a small organization, might lack the resources to do.

So the next time you are eager to install a package, check its source code. It might be a one-liner that you can write instead of introducing a potential attack risk and slowing your CI/CD pipeline.

But don't go too far with this method: you don't want to reinvent the wheel or entirely deprive yourself of the community's support.

## Method Three: Extract Sub-packages with `lodash`

Imagine we have the following object interface:

```typescript
interface SomeObject {
  foo?: {
    bar?: {
      baz?: string;
    };
  };
}
```

And we have a function that accepts an object with that interface.

```typescript
function getBazOrDefault(obj: SomeObject, defaultValue: string);
```

As you've guessed from the function's name, it will give us the value of `baz`, or `defaultValue`, if the path to `baz` is `undefined`. Here is one implementation of that function:

```typescript
function getBazOrDefault(obj: SomeObject, defaultValue: string) {
  if (!obj.foo || !obj.foo.bar || !obj.foo.bar.baz) return defaultValue;
  return obj.foo.bar.baz;
}
```

Ugly, right? It will get uglier if you need to deal with arrays. Luckily, we can use the popular [`lodash`](https://www.npmjs.com/package/lodash) library!

Install `lodash`, and the code becomes nice and easy to read:

```typescript
import _ from "lodash";

function getBazOrDefault(obj: SomeObject, defaultValue: string) {
  return _.get(obj, "foo.bar.baz", defaultValue);
}
```

Neat!

However, if we install `lodash`:

```shell
npm i lodash
```

And its type definitions (because we use TypeScript):

```shell
npm i -D @types/lodash
```

We will introduce 8.4MB of dependencies to our `node_modules`. That's a lot!

```shell
❯ du -sh node_modules
8.4M    node_modules
```

We have some experience now, so let's put it to practice!

- **Package age** - `lodash` is relatively old — it was published a year ago. I couldn't find any JavaScript core functionality to get a nested value from an object by a string key. Let's move on.
- **One-liner package** - `lodash` is not a one-liner. It has tens of files and lots of tests. Even looking at the functionality of [`_.get`](https://github.com/lodash/lodash/blob/master/get.js), it's not exactly a one-liner. While it's a simple two-line function, it has an internal dependency on `./internals/baseGet.js`, which in turn depends on `./castPath.js` and `./toKey.js` — and each depends on more files! It's too much to be a candidate for extracting standalone code.

So it seems we are stuck with `lodash` then. But wait! There is another trick I want to show you! **Sub-package extraction**. If we read the `lodash` readme carefully, we will notice that every single functionality `lodash` provides is extracted into its own [sub-package](https://www.npmjs.com/search?q=keywords:lodash-modularized), including the `_.get` function!

Instead of installing the entire `lodash` library, we can install `lodash.get` for just the `_.get` function (of course, don't forget the type definitions). This reduces the `node_modules` size from 8.4MB to 3.6MB. It's still a large folder, but a **57%** reduction from the original size! I'll happily take such a percentage reduction.

```shell
❯ npm i lodash.get
❯ npm i -D @types/lodash.get
❯ du -sh node_modules
3.6M    node_modules
```

The `getBazOrDefault` code will look like this:

```typescript
import get from "lodash.get";

function getBazOrDefault(obj: SomeObject, defaultValue: string) {
  return get(obj, "foo.bar.baz", defaultValue);
}
```

In conclusion, be aware that many packages, especially collections of utilities like `lodash`, can be published to `npm` as individual packages. Instead of installing the entire 312 different methods from `lodash`, we can install the ones we actually need, reducing the weight of our `node_modules`.

## Method Four: Do It Yourself

Last but not least is the DIY method.

Let's say we need a method to capitalize the first letter of each word in a given string. We can run an npm search for the term [capitalize](https://www.npmjs.com/search?q=capitalize) and be presented with 266 different packages (not all relevant).

We can spend some time looking for a relevant, relatively maintained package and add it as a dependency to our project. Or we can write it ourselves!

Why, you ask? Software engineering is the art of problem-solving. To become a great software engineer, you need to be able to solve software engineering problems.

If you only know how to use third-party packages and copy code from Stack Overflow, you will be a software engineer, but not a great one. And you do want to be great, don't you? Then let's write our own capitalize function!

Let's start with a signature:

```typescript
function capitalize(str: string);
```

First, let's identify words from our string. We can do that by splitting the string with whitespace:

```typescript
const words = str.split(" ");
```

Then we need to capitalize the first letter of each word:

```typescript
for (let i = 0; i < words.length; ++i) {
  words[i] = words[i].charAt(0).toUpperCase() + words[i].substring(1);
}
```

And lastly, we need to join the words back into a string:

```typescript
const capitalizedString = words.join(" ");
```

Here's the entire function:

```typescript
function capitalize(str: string) {
  const words = str.split(" ");
  for (let i = 0; i < words.length; ++i) {
    words[i] = words[i].charAt(0).toUpperCase() + words[i].substring(1);
  }
  return words.join(" ");
}
```

If you run this with some example strings:

```typescript
console.log(capitalize("hello world")); // Hello World
console.log(capitalize("how are you?")); // How Are You?
```

Great success!

**Note** - this function is for demonstration purposes only. It does not consider scenarios like quoted words — for example, `hello "world"` will be capitalized incorrectly. However, this is a great opportunity to up your problem-solving skills! Go on and figure out how to capitalize quoted words as well!

As with the second method, you'll want to avoid 'reinventing the wheel' here. On the one hand, you don't want to write the same functionality over and over. On the other hand, you don't want to introduce a dependency for every small functionality you need.

Always analyze and evaluate each solution. Many packages do way more than you need, so it's better to implement the code yourself. However, there are good quality packages for common utils like [slugify](https://www.npmjs.com/package/slugify), which is well-maintained and provides basic functionality you probably wouldn't want to implement yourself in a production environment.

## Wrap Up

In this post, we ran through four methods to analyze packages and shrink your `node_modules`:

- Checking the age of packages
- Using one-liner packages
- Extracting sub-packages
- Doing it yourself

These methods should make your development process faster, reducing the number of packages you need to pull every time you run your CI/CD pipeline.

All in all, the `npm` ecosystem is great! It is the biggest package registry as of today. But installing packages shouldn't be a panacea. Our project is not only the code we write — it's also the packages it consists of. Knowing what goes into our `node_modules` makes our projects more resilient and makes us better developers.

Until next time, happy coding!

**P.S. If you liked this post, [subscribe to our JavaScript Sorcery list](https://blog.appsignal.com/javascript-sorcery) for a monthly deep dive into more magical JavaScript tips and tricks.**

**P.P.S. If you need an APM for your Node.js app, go and [check out the AppSignal APM for Node.js](https://www.appsignal.com/nodejs).**