PNPM is a package manager that optimizes dependency management and reduces disk usage with a symlink-based approach, eliminating duplication. It's commonly used by frameworks like Vue and Svelte.
Unlike traditional managers, PNPM stores each package version once and links it as needed, preventing redundancy. It supports workspaces, strict dependency checks, fast installs, and it remains compatible with the npm registry.
This guide covers how to use PNPM for faster, more reliable workflows while avoiding common dependency issues.
Prerequisites
Before proceeding with the rest of this article, ensure you have a recent version of Node.js (version 22.x or later) installed locally on your machine. This article also assumes you are familiar with the basic concepts of package management in Node.js.
Getting started with PNPM
To get the most out of this tutorial, create a new Node.js project to try out the concepts we will be discussing.
Start by installing PNPM globally using npm:
Alternatively, you can install PNPM using other methods as described in the official documentation.
Once installed, verify that PNPM is correctly set up by checking its version:
Now, let's initialize a new project using PNPM:
This will create a basic package.json file in your project directory.
Let's now add a simple dependency to see PNPM in action:
After running this command, you'll notice that PNPM creates a node_modules directory.
Let's create a simple Express application to test our setup:
Run the application:
When you visit http://localhost:3000 in your browser, you should see "Hello from PNPM!" displayed:
Now stop the server and use Ctrl + C in the terminal.
Understanding PNPM's node modules structure
After setting up a basic project with PNPM, it's worth taking a deeper look at how PNPM organizes dependencies - one of its most innovative features.
Unlike npm or Yarn, which create flat dependency trees (often leading to "dependency hell"), PNPM creates a nested structure that more accurately represents the dependency relationships.
Let's examine the structure of our project's node_modules directory more thoroughly:
What we're seeing here are the symlinks for the nested dependencies. Notice how each package has its own isolated dependencies - for example, accepts@1.3.8 has its own mime-types and negotiator modules, while finalhandler@1.3.1 has its own set of dependencies like encodeurl and unpipe. This demonstrates how PNPM creates a true hierarchical node_modules structure that accurately reflects the dependency tree.
Interestingly, we don't see Express in this list because we're only viewing the first 10 symlinks, which happen to be deeper nested dependencies. Let's look specifically for our main dependency:
This output shows that the express module in your root node_modules directory is actually a symlink that points to the actual package stored in .pnpm/express@4.21.2/node_modules/express. This is a key aspect of how PNPM organizes dependencies.
The key aspects of this structure include:
Content-addressable storage: All packages are organized by name and version in a
.pnpmdirectory. For example, Express is stored at.pnpm/express@4.21.2/node_modules/express, with the version number directly in the path.Symlinks for direct dependencies: Packages your project depends on (like express) appear at the root of
node_modulesas symlinks pointing to their location in the.pnpmdirectory.Nested dependencies: Each package's dependencies are nested under its own
node_modulesdirectory, maintaining a clear dependency tree.
Preventing phantom dependencies
One of the most significant advantages of PNPM's approach is preventing "phantom dependencies" - a common issue where a package can access dependencies it hasn't explicitly declared in its own package.json.
To demonstrate this important difference between npm and PNPM, let's run a simple experiment:
First, exit your current directory and create a new project with npm to show the problem:
Install express with npm (which will include many dependencies):
Create a file that tries to use a dependency that Express uses but you haven't directly installed:
Run it with npm's node_modules:
Notice that the script works with npm even though you never explicitly installed mime-types! This is because of npm's flat node_modules structure, which makes all nested dependencies accessible.
Now return to your PNPM project and try the same test:
Create a test file in the PNPM project:
Run it with PNPM's node_modules structure:
With PNPM, the script fails because it can't access dependencies that weren't explicitly declared in your project's `package.json, even though Express uses mime-types.
This strict dependency checking helps prevent "it works on my machine" issues and makes your applications more reliable by ensuring all dependencies are correctly declared. If you want to use mime-types with PNPM, you need to add it explicitly:
After installing it properly, the script will work as expected:
Now your dependencies are correctly stated in your package.json, making your project more maintainable and predictable.
To better understand your project's dependencies, you can use PNPM to generate a dependency graph:
This tool helps untangle dependency relationships and identify potential issues for more complex projects.
Now that you understand PNPM's unique approach to dependency management, you explore some everyday operations for managing packages in your project.
Managing dependencies with PNPM
Now that we understand PNPM's unique approach to dependency management, let's explore common operations for managing packages in your project. PNPM provides familiar commands that mirror npm's functionality while adding some powerful enhancements.
Installing dependencies
To add a new dependency to your project, you use the add command:
This will add the lodash package to your project and update your package.json file accordingly.
To add a development dependency, use the -D flag:
You can also install a specific version of a package:
Installing all dependencies
If you've just cloned a project that uses PNPM, you can install all dependencies defined in the package.json file:
This command is also aliased as pnpm i for convenience.
Updating dependencies
To check for outdated packages:
To update all dependencies according to your version constraints in package.json:
To update a specific package:
To update a package to the latest version, ignoring version constraints:
Removing dependencies
To remove a package:
Script execution
Like npm, PNPM allows you to define scripts in your package.json file:
To run a script:
PNPM also supports several shorthand notations:
To pass arguments to scripts, use double dashes:
Listing installed packages
To see all installed packages:
For a more concise view showing only your direct dependencies:
Auditing dependencies for security issues
To check your dependencies for known security vulnerabilities:
To automatically fix issues when possible:
Working with lockfiles
PNPM generates a pnpm-lock.yaml file that records the exact version of each dependency installed in your project. This ensures that everyone working on the project gets exactly the same dependency versions.
The pnpm-lock.yaml file is automatically created and updated when you install or update dependencies. It contains:
- A record of all installed packages
- Their exact versions
- Integrity checksums to verify package content
- Dependencies of each package
Always commit the pnpm-lock.yaml file to your version control system. This ensures that:
- All developers work with the same dependency versions
- CI/CD builds are reproducible
- Production deployments use the exact dependencies you tested against
To install dependencies exactly as specified in the lock file:
This is particularly useful in CI/CD environments to ensure build consistency.
Optimizing your PNPM workflow
You can create a .npmrc file in your project to configure PNPM behavior:
Using shorter commands
PNPM commands can become tedious to type repeatedly. You can create an alias by adding the following to your shell profile (.bashrc, .zshrc, etc.):
Now you can use shorter commands like:
This minor productivity enhancement can save you many keystrokes throughout your development workflow.
Final thoughts
In this article, we've explored how PNPM addresses common pain points in JavaScript dependency management. Its innovative symlink-based approach provides true dependency isolation, preventing phantom dependencies while maintaining compatibility with the npm ecosystem.
As JavaScript projects continue to grow in complexity, adopting a package manager like PNPM can help ensure your dependency management scales with your project's needs. Consider exploring PNPM's more advanced features like workspaces.