Advanced Monorepo Patterns
Scale monorepos with NX, Changesets versioning, affected-only builds, code generators, and enforcing boundaries between packages.
Large monorepos require tools that go beyond basic workspaces: affected-only builds (only rebuild what changed), code generators (consistent scaffolding), dependency boundary enforcement, and automated versioning. NX and Changesets are the standard solutions.
Turborepo for build caching (simple). NX when you need code generators, dependency boundary enforcement, and a rich plugin ecosystem. Changesets for independent package versioning in component libraries. Never let a slow CI become a monorepo reason to split into polyrepo.
Workflow
- Use NX for affected-only buildsNX uses dependency graph analysis to determine exactly which projects need rebuilding after a change.
- Enforce package boundariesWithout boundaries, packages develop implicit dependencies that make the monorepo fragile.
- Generate code with NX generatorsGenerators ensure new packages/features follow conventions - no manual setup drift.
- Version packages with ChangesetsChangesets enables independent versioning of multiple packages with automated changelogs.
- Optimize CI for large monoreposMonorepo CI must stay fast even as the codebase grows.
Output Format
NX with affected detection and remote cache, tag-based boundary enforcement, generators for scaffolding, Changesets for versioned packages, CI with affected matrix.
Examples
NX boundary rule: `"depConstraints": [{ "sourceTag": "type:ui", "onlyDependOnLibsWithTags": ["type:util"] }, { "sourceTag": "type:feature", "onlyDependOnLibsWithTags": ["type:ui","type:util","type:data-access"] }, { "sourceTag": "type:app", "onlyDependOnLibsWithTags": ["*"] }]`Affected matrix: `matrix=$(nx show projects --affected --type=lib | jq -Rc "[.,inputs]" | jq -c "."); echo "matrix=$matrix" >> $GITHUB_OUTPUT // then: jobs: test: strategy: matrix: project: ${{ fromJson(needs.affected.outputs.matrix) }} steps: - run: nx test ${{ matrix.project }}`Changeset: `npx changeset // prompts: Which packages changed? → @myorg/ui @myorg/tokens // Bump type for @myorg/ui? → patch // Summary: Fix button focus ring in high contrast mode`Edge Cases & Gotchas
- NX graph computation: for very large repos (1000+ projects), `nx graph` computation itself can be slow. Use `nx daemon` (always-on background process) to keep the graph hot
- Changesets and private packages: private packages (apps) don't need version bumping - mark them `"private": true` in package.json and Changesets ignores them
- Root package.json scripts: avoid putting all scripts in root - it defeats NX's affected detection. Define scripts in each package and orchestrate via `nx run-many`
When to Break These Rules
- For a monorepo with <20 packages that doesn't need npm publishing, NX is overkill - Turborepo is sufficient. NX pays off at scale (50+ packages, multiple teams).
Reference Files
NX docs: https://nx.dev/docsNX Cloud: https://nx.app/Changesets: https://github.com/changesets/changesetsModule boundaries: https://nx.dev/core-features/enforce-project-boundaries