A reference implementation for building production-ready JavaScript/TypeScript monorepos using Yarn workspaces, Nx, and Changesets. This repository demonstrates best practices for dependency management, build orchestration, versioning, and automated releases in a multi-package codebase.
- Introduction
- Motivation
- Architecture
- Key Features
- Prerequisites
- Setup
- Repository Structure
- Usage
- Workflows
- Best Practices
- Scripts Reference
- CI/CD
- Contributing
- License
This monorepo demonstrates how to combine three powerful tools to create a maintainable and scalable codebase:
- Yarn (v4): Modern package manager with workspaces support and constraints enforcement
- Nx: Build system with intelligent task orchestration and caching
- Changesets: Version management and changelog generation for coordinated releases
The result is a development environment that supports multiple packages with proper dependency management, optimized builds, and automated release workflows.
Managing multiple related packages in a single repository introduces several challenges:
- Dependency Consistency: Ensuring all packages use compatible versions of shared dependencies
- Build Efficiency: Avoiding unnecessary rebuilds when only specific packages change
- Version Management: Coordinating version bumps across interdependent packages
- Release Coordination: Publishing multiple packages in the correct order
- Circular Dependencies: Detecting and preventing dependency cycles
- Code Quality: Running linting and type-checking only on affected code
This repository addresses these challenges with a combination of tooling and conventions.
The monorepo uses a layered architecture with clear separation of concerns:
apps/ # Deployable applications (private packages)
├── client/ # Frontend applications
└── server/ # Backend services
features/ # Feature-specific code (public packages)
├── client/ # UI components and features
└── server/ # Backend features and handlers
libs/ # Shared libraries (public packages)
└── validator/ # Shared validation logic
- Apps can depend on features and libs
- Features can depend on libs
- Libs should have minimal external dependencies
- No circular dependencies are allowed (enforced via script)
Constraints defined in yarn.config.cjs automatically enforce:
- Consistent Node.js version across all packages
- Consistent package manager version
- Unified dependency versions (latest version wins)
Run yarn constraints
to check violations or yarn constraints --fix
to automatically fix them.
Nx configuration in nx.json provides:
- Task Dependencies: Ensures dependencies are built before dependents
- Affected Commands: Runs tasks only for packages affected by changes
- Caching: Skips tasks when inputs haven't changed
- Parallel Execution: Runs independent tasks concurrently
Changesets configuration in .changeset/config.json enables:
- Explicit version bump declarations
- Automatic changelog generation
- Coordinated multi-package releases
- Separation between version bumps and publishing
Custom script (scripts/check-circulardeps.mjs) uses Nx's dependency graph to detect cycles:
// Generates Nx graph and performs depth-first search to find cycles
nx graph --file=nx-graph.json
Custom script (scripts/print-unpublished-packages.mjs) identifies packages that need publishing:
- Compares local package versions with npm registry
- Returns only packages with unpublished versions
- Used in CI to optimize build and publish steps
- Node.js 20.18.1 (specified in .node-version)
- Yarn 4.7.0 (managed via Corepack)
- Clone the repository:
git clone <repository-url>
cd robust-monorepo-yarn-nx-changesets
- Enable Corepack (manages Yarn version automatically):
corepack enable
- Install dependencies:
yarn install
- Verify setup:
# Check constraints
yarn constraints
# Check for circular dependencies
yarn check-circulardeps:all
# Run type checking
yarn typecheck:all
.
├── .changeset/ # Changeset configuration and pending changes
├── .github/workflows/ # CI/CD workflow definitions
├── apps/ # Application packages (private)
│ ├── client/auth-app/ # Authentication frontend app
│ └── server/auth-api/ # Authentication backend API
├── features/ # Feature packages (public)
│ ├── client/sign-in-form/ # Sign-in UI component
│ └── server/sign-in-handler/ # Sign-in backend handler
├── libs/ # Shared libraries (public)
│ └── validator/ # Validation utilities
├── scripts/ # Build and release scripts
├── .node-version # Node.js version specification
├── nx.json # Nx configuration
├── package.json # Root package configuration
├── yarn.config.cjs # Yarn constraints
└── yarn.lock # Locked dependency versions
Run type checking:
# All packages
yarn typecheck:all
# Only affected packages (compares against main branch)
yarn typecheck:affected
Run linting:
# All packages
yarn lint:all
# Only affected packages
yarn lint:affected
Run both type checking and linting:
# All packages
yarn quality:all
# Only affected packages
yarn quality:affected
Build all packages:
yarn build:all
Build only affected packages:
# Compares against main branch by default
yarn build:affected
# Compare against specific base
BASE=origin/develop yarn build:affected
Build only unpublished packages:
yarn build:unpublished
Create a changeset (declare intended version bump):
yarn changeset
This opens an interactive prompt:
- Select which packages have changes
- Choose bump type (major, minor, patch)
- Write a summary of changes
The changeset is saved in .changeset/
as a markdown file.
Apply changesets (bump versions and update changelogs):
yarn changeset version
This:
- Updates package.json versions
- Updates CHANGELOG.md files
- Deletes applied changesets
- Updates dependent package versions
Publish packages:
yarn release
This:
- Builds unpublished packages
- Publishes to npm registry
Check constraints:
yarn constraints
Fix constraint violations:
yarn constraints --fix
Check for circular dependencies:
yarn check-circulardeps:all
-
Create package directory in appropriate location (
apps/
,features/
, orlibs/
) -
Add
package.json
with required fields:name
: Must follow@robust-monorepo-yarn-nx-changesets/<package-name>
conventionversion
: Start at0.0.1
private
:true
for apps, omit for publishable packagesengines.node
: Will be set automatically by constraintspackageManager
: Will be set automatically by constraints
-
Add build and typecheck scripts:
{
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit"
}
}
- Run constraints to ensure consistency:
yarn constraints --fix
-
Make code changes in one or more packages
-
Create a changeset:
yarn changeset
- Commit changes including the changeset file:
git add .
git commit -m "feat: add new feature"
git push origin feature-branch
-
Open a pull request to main branch
-
CI checks run automatically:
- Constraint validation
- Circular dependency detection
- Type checking and linting (affected packages only)
- Version change prevention (manual version bumps are blocked)
-
After merge, the release workflow:
- Creates a "Version Packages" PR with version bumps and changelog updates
- OR publishes packages if a version PR is merged
The release process is automated via GitHub Actions:
- Merge feature branches with changesets to main
- Automated PR created titled "chore: publish new release" with:
- Version bumps for changed packages
- Updated CHANGELOG.md files
- Updated dependencies in dependent packages
- Review and merge the release PR
- Packages automatically published to npm
- Apps rebuilt and deployed if they depend on published packages
Always use affected commands in CI to reduce build times:
- run: BASE=origin/${{ github.event.pull_request.base.ref }} yarn quality:affected
This only checks packages affected by the PR changes.
Run yarn constraints
in CI to catch version inconsistencies:
- run: yarn constraints
Never commit constraint violations to main branch.
Use the provided GitHub Action (.github/workflows/quality.yaml) to block PRs that manually change package versions. All version changes must go through changesets.
Run the circular dependency check in CI:
- run: yarn check-circulardeps:all
Circular dependencies cause issues with build order and can lead to runtime errors.
The release script builds unpublished packages before publishing:
yarn build:unpublished && yarn changeset publish
This ensures only necessary builds run and all published packages include compiled code.
Reference other monorepo packages using workspace protocol:
{
"dependencies": {
"@robust-monorepo-yarn-nx-changesets/validator": "0.0.3"
}
}
Yarn automatically resolves these to local packages during development.
Nx caches task results based on inputs. This is configured in nx.json:
{
"targetDefaults": {
"build": {
"cache": true,
"outputs": ["{projectRoot}/dist"]
}
}
}
Commit the .nx
directory for remote caching (optional).
Ensure tasks run in the correct order:
{
"targetDefaults": {
"typecheck": {
"dependsOn": ["^build", "^typecheck"]
}
}
}
This means typecheck runs after all dependencies are built and type-checked.
Script | Description |
---|---|
build:all |
Build all packages in the monorepo |
build:affected |
Build only packages affected by changes (compares against main) |
build:unpublished |
Build only packages not yet published to npm |
typecheck:all |
Type-check all packages |
typecheck:affected |
Type-check only affected packages |
lint:all |
Lint all packages |
lint:affected |
Lint only affected packages |
quality:all |
Run typecheck and lint on all packages |
quality:affected |
Run typecheck and lint on affected packages |
check-circulardeps:all |
Check for circular dependencies using Nx graph |
release |
Build unpublished packages and publish to npm |
Variable | Description | Default |
---|---|---|
BASE |
Base branch/commit for affected commands | origin/main |
HEAD |
Head branch/commit for affected commands | HEAD |
Example:
BASE=origin/develop HEAD=feature-branch yarn build:affected
Runs on pull requests to main (.github/workflows/quality.yaml):
- Disable manual version changes: Fails if package.json versions are manually changed
- Check quality:
- Enable Corepack
- Install dependencies with
yarn --immutable
- Run
yarn constraints
- Run
yarn check-circulardeps:all
- Run
yarn quality:affected
(only affected packages)
Runs on push to main (.github/workflows/release.yaml):
- Release libs and features:
- Install dependencies
- Build affected packages
- Run
changesets/action
which:- Creates version PR if changesets exist
- OR publishes packages if version PR is merged
- Release apps (separate workflow):
- Rebuilds and redeploys apps that depend on published packages
Set these in GitHub repository settings:
NPM_TOKEN
: npm authentication token with publish permissionsGITHUB_TOKEN
: Automatically provided by GitHub Actions