Setting Up npm Workspaces for Small Teams
🚨 Exact Symptom
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR! Could not resolve dependency:
npm ERR! peer react@"^18.0.0" from @team/shared-ui@1.0.0
npm ERR! Conflicting peer dependency: react@17.0.2
Pipeline Impact: npm install halts, blocking CI/CD builds, local dev environments, and dependency hoisting.
🔍 Root Cause
npm v7+ enforces strict peer dependency resolution and automatic workspace linking. The ERESOLVE failure triggers when sibling packages declare mismatched version ranges, the root package.json omits the workspaces array, or internal cross-package references bypass the workspace:* protocol. Without explicit workspace declarations, npm defaults to external registry resolution instead of creating local symlinks. For foundational context on dependency tree evaluation, consult Core JavaScript Package Workflows. Misconfigured workspace arrays remain the primary failure vector; refer to Workspace Configuration Deep Dive for field-level specifications and hoisting behavior.
🛠️ CLI Debug & Resolution Pipeline
Execute these commands sequentially to isolate and resolve the dependency conflict:
- Audit Root Configuration Verify the workspace glob is explicitly declared. Without it, npm treats subdirectories as isolated projects.
cat package.json | grep -A 2 '"workspaces"'
Expected Output: "workspaces": ["packages/*"]
- Force Local Resolution via
workspace:*Protocol Replace hardcoded semver ranges in siblingpackage.jsonfiles to bypass registry lookups.
# Example patch for @team/shared-ui
sed -i 's/"@team\/shared-ui": "\^1\.0\.0"/"@team\/shared-ui": "workspace:*"/g' packages/*/package.json
- Align Peer Dependencies Mismatched peer ranges trigger strict resolution failures. Standardize across all workspaces:
grep -r '"react":' packages/*/package.json
Ensure all entries resolve to a compatible range (e.g., "react": "^18.2.0").
- Clean Slate Installation Purge stale artifacts and regenerate the unified lockfile with correct symlinks.
rm -rf node_modules package-lock.json
npm install
- Validate Workspace Topology Confirm internal packages are correctly linked and no resolution errors persist.
npm ls --workspaces --depth=0
Success Indicator: Internal packages display -> ./packages/<name> instead of a version number.
📦 Configuration Patches
Root package.json
{
"name": "@team/monorepo",
"private": true,
"workspaces": ["packages/*"],
"engines": { "node": ">=18.0.0" }
}
Subpackage package.json
{
"name": "@team/shared-utils",
"version": "1.0.0",
"main": "dist/index.js",
"dependencies": {
"@team/config": "workspace:*"
}
}
.npmrc (Optional Migration Bypass)
Apply only to temporarily bypass legacy peer conflicts during initial migration. Revert to strict mode post-stabilization.
auto-install-peers=true
strict-peer-dependencies=false
🛡️ Prevention & Pipeline Hardening
- Lockfile Integrity: Commit
package-lock.jsonto version control. Never runnpm installin CI without it; usenpm cifor deterministic installs. - CI Pre-flight: Add
npm ci --ignore-scriptsto your pipeline to guarantee artifact consistency before build/test steps. - Dependency Auditing: Run
npm ls --all --workspacesweekly to detect phantom dependencies or version drift across the tree. - Publish Safeguards: Workspace packages require individual publishing. Use
npm publish --workspace packages/<name>and ensure the rootpackage.jsoncontains"private": trueto prevent accidental root registry pushes.
❓ Frequently Asked Questions
Do small teams need a dedicated monorepo tool like Nx or Turborepo? No. Native npm workspaces handle dependency hoisting, cross-package symlinking, and unified lockfiles out-of-the-box. Advanced tools are only required for distributed caching, complex task orchestration, or build graph optimization.
How does npm handle versioning across workspace packages?
npm does not enforce synchronized versioning. Each package maintains an independent version field. Using workspace:* in dependencies guarantees the resolver always references the current local build state, ignoring published registry versions.
Why does npm publish fail for workspace packages?
The CLI prevents bulk publishing to avoid registry pollution. Execute npm publish from within the target package directory, or scope it explicitly: npm publish --workspace packages/<name>.