Managing Dependencies with NPM and Yarn Workspaces

Learn how to manage dependencies effectively with NPM and Yarn Workspaces. Optimize your monorepo or multi-package projects with these powerful tools

In modern web development, managing dependencies efficiently is a critical aspect of maintaining scalable and maintainable codebases. As projects grow in size and complexity, managing packages, libraries, and versioning can become increasingly difficult. When working with multiple related projects or monorepos, dependency management can quickly spiral out of control, leading to redundant installations, version conflicts, and slower build processes.

This is where tools like NPM Workspaces and Yarn Workspaces come into play. Both of these package managers provide powerful solutions for managing dependencies across multiple packages in a single codebase. In this article, we’ll explore how NPM and Yarn Workspaces work, the differences between them, and how to choose the right tool for your project. We’ll also walk through practical examples of setting up workspaces in both tools, ensuring you can manage dependencies effectively in your web development workflow.

What Are Workspaces?

Workspaces are a feature provided by both NPM and Yarn that allow developers to manage multiple packages within a single repository. If you’ve ever worked with a monorepo—a single repository that houses multiple related projects—then you know how complex dependency management can get. Workspaces help solve this problem by enabling you to manage dependencies for all packages within the repo from a single, centralized location.

The key advantages of using workspaces include:

Efficient Dependency Management: Instead of installing the same dependencies in multiple packages, workspaces allow you to share dependencies across projects, reducing redundancy.

Better Version Control: When you manage related packages in a single repository, you can ensure all packages are on the same version of a dependency, preventing version mismatches and conflicts.

Faster Installs and Builds: Workspaces streamline the installation process by deduplicating dependencies, leading to faster build times.

Simplified Project Structure: Workspaces allow you to organize your codebase more effectively, especially when working with shared libraries, making it easier to develop, test, and maintain multiple projects.

NPM Workspaces vs. Yarn Workspaces: Key Differences

Before diving into the details of how to use NPM and Yarn Workspaces, it’s important to understand the differences between the two. Both tools offer similar functionality, but they have different approaches and trade-offs that may influence your decision on which to use.

NPM Workspaces

Introduced in NPM 7, NPM Workspaces is a relatively new feature that enables workspace support directly in NPM. It’s tightly integrated into the NPM ecosystem, so if your project is already using NPM, adding workspaces is a natural extension.

Key features of NPM Workspaces include:

Native to NPM: Since it’s part of NPM 7 and later, you don’t need to install any additional tools or plugins. It’s fully integrated with the existing NPM ecosystem.

Ease of Use: NPM Workspaces is straightforward to set up, and it works out of the box for projects that are already using NPM for package management.

Automatic Dependency Hoisting: NPM Workspaces automatically hoists shared dependencies to the root node_modules folder, making dependency management more efficient.

Yarn Workspaces

Yarn Workspaces has been around longer and offers a more mature and robust solution for managing monorepos. Yarn’s focus on performance and reliability has made it a popular choice for large-scale projects and monorepos that require fine-grained control over dependency management.

Key features of Yarn Workspaces include:

Performance: Yarn is known for its fast installs and efficient package management. Yarn Workspaces takes this a step further by optimizing dependency resolution and hoisting, leading to even faster builds in larger projects.

Plug-and-Play (PnP): Yarn introduced a Plug-and-Play feature that eliminates the need for a node_modules folder entirely, resulting in even faster resolution and stricter control over dependencies.

Granular Control: Yarn Workspaces provides more advanced features for controlling how dependencies are shared and hoisted, making it ideal for complex monorepos with many interdependent packages.

Now that we’ve covered the basics of NPM and Yarn Workspaces, let’s explore how to set up workspaces with both tools.

Yarn Workspaces provides more advanced features for controlling how dependencies are shared and hoisted, making it ideal for complex monorepos with many interdependent packages.

Setting Up Workspaces with NPM

Let’s start by setting up NPM Workspaces. In this example, we’ll create a simple monorepo that contains two packages—a UI library and an API client—and demonstrate how to manage their dependencies using NPM Workspaces.

Step 1: Install NPM 7 or Later

First, ensure that you have NPM version 7 or later installed. You can check your NPM version by running:

npm --version

If you’re using an older version of NPM, you can update to the latest version by running:

npm install -g npm

Step 2: Set Up the Project Structure

Next, let’s create a new directory for the monorepo and define the structure for our workspace:

mkdir my-monorepo
cd my-monorepo
mkdir packages

Inside the packages folder, create two directories: ui-library and api-client:

mkdir packages/ui-library packages/api-client

Step 3: Initialize the Monorepo

Now, initialize the monorepo by creating a package.json file at the root of the project:

npm init -y

This will generate a package.json file. Open it and add the following workspaces field to declare the two packages in the packages directory:

{
"name": "my-monorepo",
"version": "1.0.0",
"workspaces": [
"packages/ui-library",
"packages/api-client"
]
}

Step 4: Initialize Each Package

Next, initialize package.json files for both the ui-library and api-client packages:

cd packages/ui-library
npm init -y
cd ../api-client
npm init -y

You now have the basic setup for your workspace, with each package having its own package.json.

Step 5: Install Dependencies

Let’s say your ui-library package depends on react and styled-components, and your api-client package depends on axios. To install these dependencies, navigate back to the root of the monorepo and install the dependencies in one go:

npm install react styled-components --workspace=packages/ui-library
npm install axios --workspace=packages/api-client

NPM Workspaces will automatically hoist shared dependencies (e.g., react) to the root node_modules folder, ensuring that both packages can share the same dependencies without duplicating installations.

Step 6: Run Scripts Across Workspaces

NPM Workspaces allows you to run scripts across all packages in the workspace. For example, if you add a build script to each package’s package.json:

In ui-library/package.json:

{
"scripts": {
"build": "echo Building UI Library"
}
}

In api-client/package.json:

{
"scripts": {
"build": "echo Building API Client"
}
}

You can run the build script for both packages from the root:

npm run build --workspaces

This will execute the build script for both the ui-library and api-client packages, streamlining the workflow for running tasks across multiple projects.

Setting Up Workspaces with Yarn

Yarn Workspaces offers a similar setup, but with some additional features for more advanced dependency management. Let’s walk through setting up the same monorepo structure using Yarn Workspaces.

Step 1: Install Yarn

First, ensure that you have Yarn installed. You can install Yarn globally by running:

npm install -g yarn

To check your Yarn version, run:

yarn --version

Step 2: Initialize the Monorepo

Just like in the NPM example, create the monorepo directory and initialize a package.json file at the root:

mkdir my-monorepo
cd my-monorepo
yarn init -y

In the root package.json, add the workspaces field:

{
"name": "my-monorepo",
"version": "1.0.0",
"workspaces": [
"packages/ui-library",
"packages/api-client"
]
}

Step 3: Set Up the Project Structure

Next, create the same project structure as before:

mkdir packages
mkdir packages/ui-library packages/api-client

Initialize each package’s package.json file:

cd packages/ui-library
yarn init -y
cd ../api-client
yarn init -y

Step 4: Install Dependencies

Now, install the dependencies for each package. Yarn will automatically hoist shared dependencies to the root node_modules folder, similar to NPM Workspaces:

yarn workspace ui-library add react styled-components
yarn workspace api-client add axios

Step 5: Run Scripts Across Workspaces

Yarn allows you to run scripts across all workspaces using the yarn workspaces run command. Add a build script to both packages, just as we did in the NPM example:

In ui-library/package.json:

{
"scripts": {
"build": "echo Building UI Library"
}
}

In api-client/package.json:

{
"scripts": {
"build": "echo Building API Client"
}
}

To run the build script for both packages, use the following command:

yarn workspaces run build

Yarn will execute the build script for both the ui-library and api-client packages, streamlining the build process for the entire monorepo.

Both NPM and Yarn Workspaces provide powerful tools for managing dependencies in a monorepo

Choosing Between NPM and Yarn Workspaces

Both NPM and Yarn Workspaces provide powerful tools for managing dependencies in a monorepo. However, the right choice depends on the specific needs of your project:

Use NPM Workspaces if you’re already using NPM and want a simple, native solution for managing dependencies. NPM Workspaces is easy to set up and works well for projects that don’t require advanced features like Plug-and-Play.

Use Yarn Workspaces if performance is a priority, especially for larger projects. Yarn’s Plug-and-Play (PnP) feature and granular control over dependency hoisting make it a better choice for complex monorepos or teams working at scale.

Both tools are reliable and will improve your dependency management workflow, so the choice ultimately depends on your project’s complexity and team preferences.

Best Practices for Managing Dependencies in Workspaces

Managing dependencies in a monorepo can be tricky, especially when you have many interdependent packages. Here are some best practices for managing dependencies with NPM and Yarn Workspaces:

Keep Dependencies Consistent: Ensure that packages sharing the same dependencies use the same version. Both NPM and Yarn automatically hoist shared dependencies, but you should still manually audit dependencies to ensure consistency.

Use nohoist for Specific Packages: If certain dependencies cause issues when hoisted, both NPM and Yarn offer the ability to exclude specific packages from hoisting using a nohoist configuration. This can be useful for dependencies that need to remain within a specific package’s node_modules.

Deduplicate Dependencies: Periodically run dependency audits (npm dedupe or yarn dedupe) to ensure your packages aren’t installing multiple versions of the same dependency unnecessarily.

Isolate Version Changes: When updating dependencies, test each package independently to ensure that version changes don’t introduce breaking changes across the monorepo.

Leverage CI/CD for Monorepos: Use continuous integration (CI) tools like GitHub Actions, CircleCI, or Travis CI to automate the build and testing process for each workspace. This ensures that changes to one package don’t inadvertently break others.

Advanced Strategies for Managing Dependencies in Large Projects

As your monorepo grows, dependency management can become even more complex, especially when dealing with multiple teams, shared libraries, and interdependent modules. In this section, we’ll explore more advanced strategies and tools for managing dependencies with NPM and Yarn Workspaces. These strategies will help you maintain clean, scalable, and maintainable codebases over time.

1. Using Peer Dependencies for Shared Libraries

When managing dependencies in monorepos, especially for shared libraries, peer dependencies can be a powerful tool. Peer dependencies ensure that a specific version of a package is available but not directly installed by the package itself. Instead, the package expects that the consuming application or project will provide the required dependency.

This is especially useful when developing shared UI components, utility libraries, or plugins that rely on common packages like react or lodash, where you want to ensure that only one version of the dependency is used across the entire monorepo.

Here’s how you can define peer dependencies in a package:

{
"name": "my-ui-library",
"version": "1.0.0",
"peerDependencies": {
"react": "^17.0.0"
}
}

By using peer dependencies, you avoid the risk of multiple versions of react being installed in different parts of your monorepo, which could lead to conflicts or bugs.

2. Scoped Packages for Better Organization

In larger projects with many internal libraries, it’s important to keep things organized. Scoped packages are a great way to do this. A scoped package in NPM or Yarn is simply a way to group related packages under a common namespace, often matching your organization or project name.

For example, if you have multiple packages in your monorepo that are related to a design system, you might scope them under @myorg/design-system, like this:

packages/
ui-library/
package.json (name: "@myorg/ui-library")
theme/
package.json (name: "@myorg/theme")

This approach makes it clear that these packages are part of the same suite of tools and can be published and versioned together. Scoped packages also help avoid naming conflicts on package registries like NPM.

To publish a scoped package, ensure that your package.json includes the scope in the name field:

{
"name": "@myorg/ui-library",
"version": "1.0.0",
"main": "index.js"
}

3. Dependency Hoisting: Controlling the Hoisting Behavior

Both NPM and Yarn Workspaces automatically hoist shared dependencies to the root node_modules folder to avoid duplicating them across packages. However, in some cases, you might need to control or prevent hoisting for specific dependencies, especially if a package relies on a particular version of a library that shouldn’t be shared with others.

You can prevent hoisting using the nohoist option in both NPM and Yarn.

In NPM Workspaces:

In NPM Workspaces, you can control hoisting by using the workspaces field in your package.json file and specifying which dependencies should not be hoisted.

Here’s an example:

{
"workspaces": {
"packages": ["packages/*"],
"nohoist": ["**/some-package"]
}
}

In this case, some-package will remain installed in its own node_modules folder, even if other packages in the workspace use it.

In Yarn Workspaces:

Yarn also supports the nohoist configuration. You can specify which dependencies should not be hoisted using a similar structure in your package.json file:

{
"workspaces": {
"packages": ["packages/*"],
"nohoist": ["**/some-package", "**/some-package/**"]
}
}

Preventing hoisting is particularly useful for packages that rely on specific versions of dependencies or need to maintain isolation from other packages in the monorepo.

Conclusion

Managing dependencies across multiple projects in a monorepo can be a complex task, but NPM and Yarn Workspaces provide elegant solutions to streamline this process. Whether you’re using NPM or Yarn, workspaces offer a way to organize related packages, share dependencies efficiently, and simplify version management. By leveraging workspaces, you can improve build performance, reduce redundancy, and ensure that your projects remain easy to maintain as they grow.

With tools like PixelFree Studio, you can take your project management and web development workflow to the next level. PixelFree Studio simplifies the process of designing and building complex web applications, while ensuring that your dependencies and workspaces are organized and efficient. Whether you’re working on a small project or managing a large-scale monorepo, understanding how to manage dependencies with workspaces will improve your development experience and help you build better, more scalable applications.

Read Next: