patch-package vs pnpm patch vs yarn patch: Patching node_modules (2026)
TL;DR
patch-package is the original tool for patching npm dependencies — edit files in node_modules/, run npx patch-package <pkg>, and it saves a .patch file that auto-applies on install. pnpm patch is pnpm's built-in patching — same workflow, integrated into the package manager, stored in patches/. yarn patch is Yarn Berry's equivalent — yarn patch <pkg> opens an editable copy, yarn patch-commit saves the diff. In 2026: use your package manager's built-in patching (pnpm patch or yarn patch) if possible, fall back to patch-package for npm.
Key Takeaways
- patch-package: ~3M weekly downloads — works with npm, yarn classic, pnpm, package-manager-agnostic
- pnpm patch: built into pnpm — zero extra dependencies,
pnpm patch/pnpm patch-commit - yarn patch: built into Yarn Berry (v2+) —
yarn patch/yarn patch-commit - Patches are git-trackable
.patchfiles — your team gets the fix automatically - All three auto-apply patches on install —
postinstallhook or built-in integration - Always file an upstream PR — patches are temporary fixes, not permanent solutions
When to Patch Dependencies
Patch when:
✅ Critical bug fix — upstream hasn't released yet
✅ TypeScript type errors — @types package has wrong types
✅ Security vulnerability — can't upgrade (breaking changes), patch the specific issue
✅ Framework compatibility — small adjustment for your specific use case
Don't patch when:
❌ Feature additions — fork the package instead
❌ Major changes — too hard to maintain across version updates
❌ Upstream fix is available — just upgrade
Always:
1. File an issue/PR upstream
2. Add a comment in the patch explaining WHY
3. Check on every dependency upgrade if the patch is still needed
patch-package
patch-package — package-manager-agnostic:
Setup
npm install -D patch-package
# Add postinstall hook to auto-apply patches:
# package.json:
{
"scripts": {
"postinstall": "patch-package"
}
}
Create a patch
# 1. Edit the file directly in node_modules:
# Fix the bug in node_modules/some-package/dist/index.js
# 2. Create the patch:
npx patch-package some-package
# Creates: patches/some-package+1.2.3.patch
# This is a standard unified diff file
# 3. Commit the patch:
git add patches/
git commit -m "fix: patch some-package to fix XYZ bug"
Example: fixing a TypeScript type
# Problem: @types/express has wrong types for req.query
# Edit: node_modules/@types/express/index.d.ts
# Create scoped package patch:
npx patch-package @types/express
# Creates: patches/@types+express+4.17.21.patch
Example patch file
# patches/some-package+1.2.3.patch
diff --git a/node_modules/some-package/dist/index.js b/node_modules/some-package/dist/index.js
index abc1234..def5678 100644
--- a/node_modules/some-package/dist/index.js
+++ b/node_modules/some-package/dist/index.js
@@ -42,7 +42,7 @@ function processData(input) {
- if (input.length = 0) {
+ if (input.length === 0) {
return [];
}
Version-specific patches
# Patch applies to exact version by default:
# patches/react-dom+18.2.0.patch → only applies to 18.2.0
# When you upgrade react-dom to 18.3.0:
# patch-package will WARN if the patch doesn't apply cleanly
# → Review and recreate the patch if needed
pnpm patch
pnpm patch — built-in pnpm patching:
Workflow
# 1. Start patching — pnpm creates an editable copy:
pnpm patch some-package@1.2.3
# Output:
# You can now edit the following folder: /tmp/.pnpm-patch-xxx/some-package
# 2. Edit the file in the temporary folder:
# Edit /tmp/.pnpm-patch-xxx/some-package/dist/index.js
# 3. Commit the patch:
pnpm patch-commit /tmp/.pnpm-patch-xxx/some-package
# Creates: patches/some-package@1.2.3.patch
# Updates package.json:
package.json integration
{
"pnpm": {
"patchedDependencies": {
"some-package@1.2.3": "patches/some-package@1.2.3.patch"
}
}
}
Auto-application
# Patches apply automatically on pnpm install:
pnpm install
# → Applying patches...
# → Patched some-package@1.2.3
# No postinstall script needed — pnpm handles it natively
Removing a patch
# Remove from patchedDependencies in package.json
# Delete the patch file:
rm patches/some-package@1.2.3.patch
# Reinstall:
pnpm install
yarn patch
yarn patch — Yarn Berry (v2+) built-in:
Workflow
# 1. Start patching:
yarn patch some-package
# Output:
# ➤ YN0000: Package some-package@npm:1.2.3 got extracted to /tmp/xfs-xxx
# 2. Edit files in the extracted folder:
# Edit /tmp/xfs-xxx/dist/index.js
# 3. Save the patch:
yarn patch-commit -s /tmp/xfs-xxx
# Creates: .yarn/patches/some-package-npm-1.2.3-xxxxx.patch
# Updates package.json:
package.json integration
{
"resolutions": {
"some-package@1.2.3": "patch:some-package@npm%3A1.2.3#~/.yarn/patches/some-package-npm-1.2.3-xxxxx.patch"
}
}
Auto-application
# Patches apply automatically on yarn install:
yarn install
# → some-package patched
# No postinstall hook needed
Best Practices
Always document your patches
# In your codebase, create a PATCHES.md or add comments:
# patches/some-package+1.2.3.patch
# WHY: Fixes null reference when input is empty array
# UPSTREAM: https://github.com/owner/some-package/issues/123
# PR: https://github.com/owner/some-package/pull/456
# REMOVE WHEN: some-package >= 1.2.4 is released
CI verification
// package.json (patch-package):
{
"scripts": {
"postinstall": "patch-package",
"verify-patches": "patch-package --error-on-fail"
}
}
# GitHub Actions:
- name: Install dependencies
run: npm ci
# postinstall runs patch-package automatically
- name: Verify patches applied
run: npx patch-package --error-on-fail
# Fails CI if any patch doesn't apply cleanly
Monorepo patches
# patch-package in monorepo (from root):
npx patch-package some-package --patch-dir patches/
# pnpm workspace — patches defined in root package.json:
{
"pnpm": {
"patchedDependencies": {
"some-package@1.2.3": "patches/some-package@1.2.3.patch"
}
}
}
Feature Comparison
| Feature | patch-package | pnpm patch | yarn patch |
|---|---|---|---|
| Package manager | npm, yarn, pnpm | pnpm only | Yarn Berry only |
| Extra dependency | Yes | No (built-in) | No (built-in) |
| Workflow | Edit node_modules → patch | pnpm patch → edit copy → commit | yarn patch → edit copy → commit |
| Auto-apply | postinstall hook | Built-in | Built-in |
| Config location | patches/ dir | package.json patchedDependencies | package.json resolutions |
| Scoped packages | ✅ | ✅ | ✅ |
| Monorepo | ✅ | ✅ | ✅ |
| Patch on fail | --error-on-fail | Built-in error | Built-in error |
| Weekly downloads | ~3M | built-in | built-in |
When to Use Each
Use patch-package if:
- Using npm as your package manager (npm has no built-in patching)
- Need a tool that works across npm, yarn classic, and pnpm
- Team has mixed package manager usage
Use pnpm patch if:
- Already using pnpm — zero extra dependencies, native integration
- pnpm's
patchedDependenciesconfig is clean and declarative - Patches apply automatically without postinstall hooks
Use yarn patch if:
- Using Yarn Berry (v2+) — built-in, native workflow
- Yarn's resolution protocol handles patching transparently
General advice:
- Always file upstream PRs — patches are temporary
- Add comments explaining why each patch exists
- Review patches on every dependency upgrade
- Keep patches small — if you need major changes, fork instead
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on patch-package v8.x, pnpm v9.x, and yarn v4.x.
Compare package management and developer tooling on PkgPulse →