Handbook
Package Managers

A Practical Guide to Frontend Package Managers

Last updated by Noel Varanda (opens in a new tab),
Frontend
Package managers
npm
yarn
pnpm

Choosing the right package manager is crucial for efficient dependency management, but each package manager has it's pros/cons and quirks.

What is a package manager?

A package manager is a tool that simplifies the process of managing and installing external libraries, frameworks, and dependencies required for a web application. It automates the retrieval, installation, and version control of these packages. This allows developers to focus on building their application rather than manually managing dependencies.

Commonly used package managers in the front-end development ecosystem include npm (Node Package Manager), Yarn, and pnpm.

What they solve

  • Dependency Management

    Automates installation of external libraries. Resolves and manages dependencies for efficient development.
  • Version Control

    Ensures consistent dependency versions. Allows easy updates while maintaining backward compatibility.
  • Centralized Repository

    Online catalog of packages. Simplifies package discovery, documentation access, and community ratings.
  • Package Scripts

    Defines automated tasks like building, testing, and deployment for streamlined development workflow.
  • Custom Configuration

    Tailors package manager behavior through project-specific settings in configuration files.
  • Ecosystem and Community Support

    Access to a vast ecosystem of open-source packages. Community-driven maintenance, updates, and support.

Package managers anatomy

A package manager is a crucial tool in front-end development for managing dependencies, automating tasks, and streamlining workflows. Let's explore the key components that make up the anatomy of a package manager.

package.json

The package configuration defines the project's metadata, dependencies, and scripts. The central file for package configuration is package.json. It contains essential information such as the project name, version, description, and a list of required dependencies.

Read npm's package.json docs (opens in a new tab) for a detailed guide on package.json.

Dependency management

The dependencies section in package.json specifies the required external libraries and frameworks for the project. It includes the name of each package and the corresponding version range or specific version required.

dependenciesThese are necessary for the application to run correctly. They are installed in the production environment. E.g. A web application using Express framework requires the express package as a dependency to handle server-side routing.
devDependenciesThese are used during development and are not necessary for the production environment. They typically include tools, testing frameworks, and other development-related dependencies. E.g. A testing library like Jest is listed as a devDependency in package.json to enable unit testing during the development process.
peerDependenciesThese specify dependencies that must be installed by the consumer of the package. They are used when a package expects a specific version of another package to be present in the consumer's environment. E.g. A library my-library requires a specific version of react to be installed in the consumer's project. By listing react as a peerDependency in package.json, my-library ensures that the correct version of react is installed, allowing for compatibility and proper functioning of the library.
Semantic versioning

Semantic Versioning (opens in a new tab) (SemVer), is a versioning scheme following a format of MAJOR.MINOR.PATCH to indicate backward-compatible changes, new features, and bug fixes, respectively. SemVer allows developers to define version ranges or specify exact versions for dependencies to ensure compatibility and manage updates effectively.

For example:

  • ^1.2.3 specifies a range where any compatible version starting from 1.2.3 up to, but not including, 2.0.0 is allowed.
  • ~1.2.3 specifies a range where any compatible version starting from 1.2.3 up to, but not including, 1.3.0 is allowed.
  • 1.2.3 specifies an exact version, ensuring the project uses that specific version of the dependency. SemVer helps package managers resolve dependencies accurately, ensuring that compatible versions are installed while avoiding breaking changes or conflicts.
Scripting capabilities

The scripts section in package.json allows developers to define custom commands that automate various tasks. These scripts can be executed via the package manager's CLI, enabling convenient build processes, testing, linting, and deployment.

Dependency resolution

Package managers handle dependency resolution to ensure consistent and compatible versions of packages within a project.

By effectively utilizing dependency trees and lock files, package managers ensure reliable and consistent resolution of dependencies, leading to stable and reproducible development environments.

Dependency tree

The dependency tree represents the hierarchical structure of the project's dependencies and their interdependencies. It visualizes how packages rely on one another and helps manage conflicts and version constraints.

Consider the following example of a simplified dependency tree:

- Project
  - Package A@1.0.0
    - Package B@^2.0.0
      - Package C@^1.1.0
  - Package D@~2.5.0
    - Package E@^3.0.0
Lock files

Lock files, such as package-lock.json (used by npm), pnpm-lock.yaml (used by pnpm), or yarn.lock (used by Yarn) play a vital role in dependency resolution. They record the exact versions of the installed packages and their dependencies, ensuring reproducibility and preventing unexpected changes during subsequent installations.

Here's an example snippet:

package-lock.json
{
  "name": "project-name",
  "version": "1.0.0",
  "lockfileVersion": 2,
  "dependencies": {
    "package-a": {
      "version": "1.0.0",
      "requires": {
        "package-b": "^2.0.0"
      }
    },
    "package-b": {
      "version": "2.1.1",
      "requires": {
        "package-c": "^1.1.0"
      }
    },
    "package-c": {
      "version": "1.2.3"
    }
  }
}

Lock files ensure that subsequent installations of the project's dependencies are based on the same versions, avoiding inconsistencies and unexpected behavior due to version changes.

Package installation

Package managers handle the installation and retrieval of packages from a central repository.

Registry

The package manager's registry serves as a centralized repository for packages. It contains a vast collection of open-source libraries and frameworks that developers can access and include in their projects. The registry allows package managers to retrieve and download packages, resolve dependencies, and manage versioning.

For example:

  • npm (Node Package Manager) uses the npm registry (registry.npmjs.org) by default.
  • Yarn uses the Yarn registry (registry.yarnpkg.com) by default.
  • pnpm uses the npm registry (registry.npmjs.org) by default.

Cache

Package managers often maintain a local cache of downloaded packages. This cache speeds up subsequent installations and reduces reliance on network requests by storing the packages locally. When a package is required, the package manager checks the cache first before fetching it from the registry.

Command-Line interface

The package manager's CLI provides a command-line interface to interact with the tool's functionality.

CLI commands

Package managers offer a variety of commands to perform common tasks such as installing packages, updating dependencies, executing scripts, and managing the package manager's configuration. For example npm install, or yarn upgrade.

Check out the CLI Commands cheatsheet for a quick reference.

Customization

The CLI allows developers to customize the behavior of the package manager using configuration files. These files enable developers to define settings such as package sources, authentication credentials, and proxy configurations.

For example, npm uses a .npmrc file to customize the configuration. Developers can specify settings like the registry URL, authentication tokens, and proxy configurations in this file. Similarly, Yarn uses a .yarnrc file for configuring various options.

Here's an example of an .npmrc file:

.npmrc
# Custom registry source
registry=https://my-custom-registry.com/
 
# Authentication token
//my-custom-registry.com/:_authToken=1234567890
 
# Proxy settings
https-proxy=http://proxy.example.com/
http-proxy=http://proxy.example.com/

By modifying these configuration files, you can tailor the package manager's behavior to your project's requirements, such as using a custom registry, applying authentication tokens, or configuring proxy settings.

Comparing npm, Yarn, and pnpm

Package managers have played a vital role in the development and management of software projects over the years. Let's explore a brief history of package managers and their evolution, focusing on three popular package managers: npm, Yarn, and pnpm.

npm

npm (opens in a new tab) (Node Package Manager) was created in 2010 as the default package manager for Node.js. It is the most widely used in the frontend community. It comes bundled with Node.js and provides a vast ecosystem of packages. Installing dependencies with npm is as simple as running npm install.

It quickly gained popularity, largely due to the rise of Node.js as a popular runtime environment for JavaScript. The npm registry became the go-to source for open-source packages, contributing to the growth of a vast ecosystem of libraries and frameworks.

Pros

  • Widely adopted package manager with a large ecosystem of packages.

  • Default package manager for Node.js and widely used in the frontend community.

  • Active community support and frequent package updates.

Yarn

Yarn (opens in a new tab) was developed by Facebook in 2016 (although it is no longer operated by Facebook (opens in a new tab)) and emerged as a response to certain limitations and challenges faced by npm.

Yarn introduced parallel package installations, significantly improving installation speed. It brought deterministic dependency resolution via a lock file (yarn.lock) that ensured consistent installations across different environments. It also introduced caching dependencies, Yarn enabled offline installations, allowing developers to install packages without an internet connection. And lastly it brought Plug-and-Play (PnP), enabling packages to be used without explicit installation, reducing disk space usage and speeding up the development process.

Read yarn vs. npm (opens in a new tab) for a full description

Pros

  • Optimized for performance with parallel installations and a global package cache.

  • Workspaces support for managing multiple packages within a single repository.

  • Plug-and-Play (PnP) feature enables packages to be used without explicit installation, reducing disk space usage.

pnpm

pnpm (opens in a new tab) was introduced in 2016 and differentiates itself by optimizing disk space usage and installation time. It also provides excellent support for monorepos and workspaces, making it efficient to manage multiple packages within a single repository, and unlike npm and Yarn, pnpm is registry-agnostic, allowing developers to configure and use any compatible registry as their package source.

Original versions of both yarn and npm stored dependencies in a project-specific node_modules directory. This means that when multiple projects have the same or overlapping dependencies, each project ends up with its own copy of those dependencies,leading to significant duplication of packages.

💡

Newer versions of yarn tackle this through their Plug'n'Play (opens in a new tab) feature.

Pros

  • Unique approach with shared dependencies to reduce disk space usage.

  • Faster installation time through the use of hard links and symlinks.

  • Excellent support for monorepos and workspaces.

  • Registry-agnostic, allowing configuration and usage of any compatible registry as a package source.

Read about pnpm's significant speed advantage

In contrast to yarn and npm's approach using node_modules, pnpm introduced a shared package store approach. Instead of duplicating packages across projects, pnpm maintains a central store of packages that can be shared among multiple projects. This is achieved by using symbolic links (opens in a new tab) to reference the packages from the shared store.

The node_modules duplication becomes even more problematic when the doppelganger issue (opens in a new tab) is introduced.

For example, consider yarn and npm's approach using node_modules. Here two projects, Project A and Project B, with the same dependency, "Example Package":

Project A
├── package.json
└── node_modules
    └── example-package

Project B
├── package.json
└── node_modules
    └── example-package

pnpm however has a shared store of packages, and uses symbolic links to reference the packages from the shared store:

Shared Package Store
└── example-package

Project A
├── package.json
└── node_modules
    └── .pnpm
        └── example-package -> ../../Shared Package Store/example-package

Project B
├── package.json
└── node_modules
    └── .pnpm
        └── example-package -> ../../Shared Package Store/example-package

Feature comparison

FeaturenpmYarnpnpm
PopularityHighHighGrowing
EcosystemLargeLargeExpanding
Dependency Managementpackage.jsonpackage.jsonpackage.json
CLIComprehensiveSimilar to npmSimilar to npm
Package ScriptsNot specificNot specific
Registrynpm registryYarn registryCustomizable
SpeedModerateFastVery Fast
Lock Filepackage-lock.jsonyarn.lockpnpm-lock.yaml
PerformanceModerateFastVery Fast
Disk SpaceModerateModerateLow
Offline SupportLimited (with npm ci)
Parallel Installs
Workspaces✅ (since npm 7)
Plug-and-Play (PnP)
Monorepo Support
Community SupportActiveActiveGrowing

Keep up to date with any latest changes or announcements by subscribing to the newsletter below.