Skip to main content
Technology & EngineeringMonorepo311 lines

Dependency Management

Managing internal package dependencies, versioning strategies, and dependency graph health in monorepos

Quick Summary34 lines
You are an expert in managing internal and external dependencies within monorepo architectures, including package linking, version alignment, and dependency graph health.

## Key Points

- name: Check circular dependencies
1. **Define clear package boundaries** — Every package should have a single purpose and a clean public API exported from `index.ts`.
2. **Use peer dependencies for singletons** — React, framework stores, and other packages that must be single instances should be peer deps.
3. **Align external versions** — Use catalogs, syncpack, or Renovate grouping to prevent version drift across packages.
4. **Enforce dependency direction** — Use lint rules or Nx boundaries to prevent apps from being imported by libs, or lower-level libs from importing higher-level ones.
5. **Detect circular dependencies in CI** — Circular deps break build ordering and create subtle runtime issues. Fail CI if any are found.
6. **Use `exports` field** — Define explicit entry points to prevent consumers from reaching into internal files.
7. **Minimize cross-package dependencies** — Each additional edge in the dependency graph reduces parallelism and increases the blast radius of changes.
- **Duplicated framework instances** — If React appears multiple times in the bundle, hooks break. Use peer deps and check with `npm ls react`.
- **Barrel file bloat** — A top-level `index.ts` that re-exports everything forces bundlers to load the entire package. Use subpath exports for large packages.
- **Version mismatches in lockfile** — Different packages requesting different ranges of the same dep can cause multiple installed versions. Use syncpack to detect this.
- **Forgetting to build dependencies first** — If `@myorg/ui` depends on `@myorg/utils`, building `ui` before `utils` fails. Configure your task runner's topological ordering.

## Quick Example

```bash
npx syncpack list-mismatches    # Find version mismatches
npx syncpack fix-mismatches     # Align all to the highest version
npx syncpack set-semver-ranges  # Normalize range format
```

```bash
# Using madge
npx madge --circular --extensions ts,tsx packages/

# Using dpdm
npx dpdm --circular --tree false packages/core/src/index.ts
```
skilldb get monorepo-skills/Dependency ManagementFull skill: 311 lines
Paste into your CLAUDE.md or agent config

Dependency Management — Monorepo Management

You are an expert in managing internal and external dependencies within monorepo architectures, including package linking, version alignment, and dependency graph health.

Core Philosophy

Overview

Dependency management in a monorepo involves coordinating how packages reference each other (internal dependencies), how external dependency versions are aligned across packages, and how the dependency graph stays clean and acyclic. Good dependency management is the foundation of a healthy monorepo: it enables caching, parallel builds, and independent deployability.

Setup & Configuration

Internal Package Structure

packages/
  core/           # No internal deps — leaf package
  utils/          # Depends on core
  ui/             # Depends on core, utils
  feature-auth/   # Depends on ui, utils
apps/
  web/            # Depends on ui, feature-auth
  api/            # Depends on core, utils

Declaring Internal Dependencies

With pnpm workspaces:

{
  "name": "@myorg/ui",
  "dependencies": {
    "@myorg/core": "workspace:*",
    "@myorg/utils": "workspace:*"
  }
}

With npm/Yarn workspaces:

{
  "name": "@myorg/ui",
  "dependencies": {
    "@myorg/core": "*",
    "@myorg/utils": "*"
  }
}

Package Exports

Every internal package should define clear entry points:

{
  "name": "@myorg/utils",
  "main": "./dist/index.js",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": {
        "types": "./dist/index.d.mts",
        "default": "./dist/index.mjs"
      },
      "require": {
        "types": "./dist/index.d.ts",
        "default": "./dist/index.js"
      }
    },
    "./math": {
      "import": "./dist/math.mjs",
      "require": "./dist/math.js",
      "types": "./dist/math.d.ts"
    }
  }
}

TypeScript Project References

For type-safe internal dependencies without building:

// apps/web/tsconfig.json
{
  "extends": "@myorg/typescript-config/react.json",
  "compilerOptions": {
    "composite": true
  },
  "references": [
    { "path": "../../packages/ui" },
    { "path": "../../packages/utils" }
  ]
}

Core Patterns

Development-Time Linking (Source Imports)

For faster dev loops, point to source instead of built output:

{
  "name": "@myorg/ui",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js"
    }
  },
  "publishConfig": {
    "main": "./dist/index.js",
    "exports": {
      ".": {
        "import": "./dist/index.mjs",
        "require": "./dist/index.js"
      }
    }
  }
}

With bundler aliases (e.g., Next.js):

// next.config.js
const path = require('path');

module.exports = {
  transpilePackages: ['@myorg/ui', '@myorg/utils'],
};

External Dependency Version Alignment

Using pnpm catalogs (pnpm 9+):

# pnpm-workspace.yaml
catalog:
  react: ^18.3.0
  react-dom: ^18.3.0
  zod: ^3.23.0
  typescript: ^5.5.0

Using syncpack:

npx syncpack list-mismatches    # Find version mismatches
npx syncpack fix-mismatches     # Align all to the highest version
npx syncpack set-semver-ranges  # Normalize range format

.syncpackrc.json:

{
  "versionGroups": [
    {
      "label": "Use workspace protocol for internal packages",
      "packages": ["@myorg/**"],
      "dependencies": ["@myorg/**"],
      "dependencyTypes": ["prod", "dev"],
      "pinVersion": "workspace:*"
    },
    {
      "label": "Pin React version across all packages",
      "dependencies": ["react", "react-dom"],
      "pinVersion": "^18.3.0"
    }
  ]
}

Using Renovate with group rules:

{
  "packageRules": [
    {
      "groupName": "react",
      "matchPackageNames": ["react", "react-dom", "@types/react", "@types/react-dom"],
      "matchUpdateTypes": ["major", "minor", "patch"]
    },
    {
      "groupName": "testing",
      "matchPackageNames": ["vitest", "@testing-library/*"],
      "matchUpdateTypes": ["minor", "patch"]
    }
  ]
}

Dependency Graph Validation

With Nx module boundaries:

{
  "rules": {
    "@nx/enforce-module-boundaries": [
      "error",
      {
        "depConstraints": [
          { "sourceTag": "type:app", "onlyDependOnLibsWithTags": ["type:feature", "type:ui", "type:util"] },
          { "sourceTag": "type:feature", "onlyDependOnLibsWithTags": ["type:ui", "type:util"] },
          { "sourceTag": "type:ui", "onlyDependOnLibsWithTags": ["type:util"] },
          { "sourceTag": "type:util", "onlyDependOnLibsWithTags": ["type:util"] }
        ]
      }
    ]
  }
}

With eslint-plugin-import for barrel file control:

// Prevent deep imports into internal modules
'import/no-internal-modules': ['error', {
  allow: ['@myorg/*/src/**']
}],
'no-restricted-imports': ['error', {
  patterns: [{
    group: ['@myorg/*/src/*'],
    message: 'Import from package root, not internal paths.'
  }]
}],

Peer Dependencies for Shared Singletons

Packages that must share a single instance (React, Zustand stores):

{
  "name": "@myorg/ui",
  "peerDependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  },
  "peerDependenciesMeta": {
    "react-dom": {
      "optional": true
    }
  },
  "devDependencies": {
    "react": "^18.3.0",
    "react-dom": "^18.3.0"
  }
}

Circular Dependency Detection

# Using madge
npx madge --circular --extensions ts,tsx packages/

# Using dpdm
npx dpdm --circular --tree false packages/core/src/index.ts

Add to CI:

- name: Check circular dependencies
  run: npx madge --circular --extensions ts,tsx packages/

Best Practices

  1. Define clear package boundaries — Every package should have a single purpose and a clean public API exported from index.ts.
  2. Use peer dependencies for singletons — React, framework stores, and other packages that must be single instances should be peer deps.
  3. Align external versions — Use catalogs, syncpack, or Renovate grouping to prevent version drift across packages.
  4. Enforce dependency direction — Use lint rules or Nx boundaries to prevent apps from being imported by libs, or lower-level libs from importing higher-level ones.
  5. Detect circular dependencies in CI — Circular deps break build ordering and create subtle runtime issues. Fail CI if any are found.
  6. Use exports field — Define explicit entry points to prevent consumers from reaching into internal files.
  7. Minimize cross-package dependencies — Each additional edge in the dependency graph reduces parallelism and increases the blast radius of changes.

Common Pitfalls

  • Importing from src/ across packages — Always import from the package name (@myorg/utils), never from relative paths to source. Use transpilePackages if you need source-level imports in dev.
  • Duplicated framework instances — If React appears multiple times in the bundle, hooks break. Use peer deps and check with npm ls react.
  • Barrel file bloat — A top-level index.ts that re-exports everything forces bundlers to load the entire package. Use subpath exports for large packages.
  • Version mismatches in lockfile — Different packages requesting different ranges of the same dep can cause multiple installed versions. Use syncpack to detect this.
  • Forgetting to build dependencies first — If @myorg/ui depends on @myorg/utils, building ui before utils fails. Configure your task runner's topological ordering.

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add monorepo-skills

Get CLI access →