September 19th, 2023

Nx, Monorepos, and Rethinking JavaScript Package Management

In the fast-paced world of JavaScript development, we often sprint towards project deadlines, relying on packages like lifelines. Have you considered whether you're truly steering your project's course, or just accumulating fragile dependencies? Explore how Nx and the monorepo approach can empower you to find a balance between innovation and an over-reliance on external code.

It's somewhat ironic that I'm writing this article shortly after publishing a GraphQL code generation plugin for Nx; however, this is a topic I've been contemplating for some time.

Packages, arguably the lifeblood of the JavaScript ecosystem, serve as the foundation for its incredible growth and adaptability. The goal of package managers and registries, which compile snippets of code into reusable utilities, each adhering to a Unix-like philosophy of doing one thing only and doing it well, is a logical approach. However, it poses a fundamental question: how many packages should one project depend on, and at what point does this dependency sprawl become a liability?

In practice, many projects grapple with an over-dependence on external code. Packages are often hastily added to address immediate needs and meet tight project deadlines. The act of searching a package registry and installing a package has become so ingrained in the development workflow that it prompts us to consider whether teams ask these types of questions often enough about their package selection.

With the help of Nx and the monorepo development paradigm, teams can take a page out of Google's playbook to address this challenge.

Internal Versus External Code

I recall listening to a talk by Stephen Fluin when he was still a member of the Angular team, describing Google's philosophy of maintaining a high level of trust for internal code and a low level of trust for external code. He detailed a workflow in which external source code is imported into Google's monorepo, making it maintainable and consumable as internal code, effectively eliminating any true external dependencies.

Initially, this approach may seem extreme and impractical for most teams outside of Google. However, with the maturity of monorepo tooling like Nx, any team is now empowered to adopt this strategy for their own package management.

Balancing Installation and Inspiration

In a monorepo environment, teams have the advantage of embracing an "install or inspire" approach to package management. After all, code packaging was initially designed for seamless distribution and sharing across multiple repositories, whereas integrated monorepos allow teams to take an internal, colocated code approach. Unfortunately, instinctive habits cause this paradigm to be overlooked, and even monorepos end up with an exceedingly large number of dependencies that might otherwise be avoided.

Certainly, there are packages that should be installed without question. When a team chooses to use Angular or React, there is no reason to deliberate over whether the necessary dependencies should be installed. The "inspire" approach should be applied to utility libraries added to projects to meet specific needs, often in limited contexts. In my opinion, these scenarios warrant a conversation about the trade-offs between installing an external dependency versus using the package to inspire an internal solution.

Consider the classnames library, often employed in React projects for conditionally joining class names into a single string. It's an extremely valuable utility, comprising perhaps no more than 50 lines of code. Should this be an external dependency of your workspace, or can you easily craft an internal solution that aligns with your team's unique requirements, using classnames to inspire your solution?

This example may seem contrived, yet it prompts us to question whether teams ask these types of questions often enough. The more your team is encouraged to find inspired solutions to common internal code challenges, the more in control they are of their code.

The "Done Versus Stale Paradox"

Another often hotly debated topic among developers regarding package management is the reliability of stale or unmaintained packages and the quest for alternatives. Let's revisit the classnames package as an example. As of the writing of this article, the package hasn't seen an update in the past year, and there are eight open issues.

Is this package now considered stale, or can it be considered "done"? As a developer considering installing classnames, how significant are the lack of updates, and will any of the open issues impact your project? How much time will you and your team potentially invest in analyzing this while also exploring alternative options that appear to be more active? Could a member of your team have written 50 or fewer similar lines of code to solve this problem internally faster?

Again, this is a contrived example; however, from personal experience, I can confidently say that adopting the Google philosophy of incorporating external code into your workspace helps alleviate these concerns almost entirely. When you find a solution to a given problem outside of your organization, using it to inspire an internal solution gives your team complete control over the stability and lifecycle of that dependency. Projects in a monorepo are rarely considered stale or abandoned; it’s much easier to label them 'done' and enhance them when needed.

It also helps to avoid the “we need a replacement” situation that arises when an external dependency is in some way flagged as stale or no longer suitable. Sticking with the classnames example, clsx advertises itself as a “faster and smaller drop-in replacement for the classnames module.” Again, I have no issue with this library or deny the efficiency it adds. At the same time, I think teams that invest the time to maintain a utility like this on their own will discover the efficiencies that make the most sense for their projects organically and not have to keep up with the ever-changing landscape of conditional class name construction.

It's actually quite ironic how quickly and easily development teams can identify internal code and label it "done" while implicitly considering packages with infrequent commits as abandoned or stale without really considering that the maintainer of that package may have deemed it "done" per their own internal metrics. Returning to the classnames example, how much more can you add to a library that conditionally concatenates strings? Every edge case, including yours, is likely not worth the maintainer's time to address.

Viva Las Packages

In no way am I anti-package or arguing that packages are not necessary. I use packages daily and am guilty of hastily installing dependencies to maintain velocity myself. However, I do hope that I've given you and your team a possible strategy to help reduce the overall number of external dependencies you rely on.

Kennie Davis

Hi, I'm Kennie Davis

An Arizona native, father of three, and software engineer passionate about Nx, React, GraphQL and improving both the developer and user experience. I write about what I'm working on and learning along the way.