Use when designing monorepo structure, organizing packages, or migrating to monorepo architecture with architectural patterns for managing dependencies and scalable workspace configurations.
Limited to specific tools
Additional assets for this skill
This skill is limited to using the following tools:
This skill provides comprehensive guidance on designing and structuring monorepos, including workspace organization, dependency management, versioning strategies, and architectural patterns that scale from small projects to enterprise applications.
A monorepo is beneficial when:
A polyrepo is beneficial when:
Monorepo Advantages:
Monorepo Challenges:
Organize by technical layer or package type.
my-monorepo/
├── apps/
│ ├── web/ # Next.js web application
│ ├── mobile/ # React Native app
│ └── api/ # Node.js API server
├── packages/
│ ├── ui/ # Shared UI components
│ ├── utils/ # Shared utilities
│ ├── config/ # Shared configurations
│ └── types/ # Shared TypeScript types
├── services/
│ ├── auth/ # Authentication service
│ ├── payments/ # Payment processing
│ └── notifications/ # Notification service
└── tooling/
├── eslint-config/ # ESLint configuration
└── tsconfig/ # TypeScript configuration
Best for: Technical separation, shared library focus, platform diversity.
Organize by business domain or feature.
my-monorepo/
├── domains/
│ ├── user/
│ │ ├── api/ # User API
│ │ ├── web/ # User web UI
│ │ ├── mobile/ # User mobile UI
│ │ └── shared/ # User shared code
│ ├── billing/
│ │ ├── api/
│ │ ├── web/
│ │ └── shared/
│ └── analytics/
│ ├── api/
│ ├── web/
│ └── shared/
└── shared/
├── ui/ # Cross-domain UI components
├── utils/ # Cross-domain utilities
└── config/ # Cross-domain config
Best for: Domain-driven design, team ownership by feature, microservices architecture.
Combine package-based and domain-based approaches.
my-monorepo/
├── apps/
│ ├── customer-portal/ # Customer-facing app
│ └── admin-dashboard/ # Admin app
├── features/
│ ├── auth/ # Authentication feature
│ ├── checkout/ # Checkout feature
│ └── inventory/ # Inventory feature
├── packages/
│ ├── ui/ # Shared UI library
│ ├── api-client/ # API client library
│ └── analytics/ # Analytics library
└── infrastructure/
├── database/ # Database utilities
├── messaging/ # Message queue
└── deployment/ # Deployment configs
Best for: Complex organizations, balancing technical and domain concerns.
Basic workspace setup using native NPM workspaces.
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"dev": "npm run dev --workspaces",
"build": "npm run build --workspaces",
"test": "npm run test --workspaces"
},
"devDependencies": {
"typescript": "^5.3.0",
"eslint": "^8.54.0"
}
}
Key Features:
Yarn's workspace implementation with additional features.
{
"name": "my-monorepo",
"private": true,
"workspaces": {
"packages": [
"apps/*",
"packages/*"
],
"nohoist": [
"**/react-native",
"**/react-native/**"
]
},
"packageManager": "yarn@3.6.4"
}
With .yarnrc.yml for Yarn Berry:
nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: "@yarnpkg/plugin-workspace-tools"
enableGlobalCache: true
compressionLevel: mixed
Key Features:
PNPM's efficient workspace implementation.
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
- 'services/*'
- '!**/test/**'
With workspace-specific .npmrc:
# .npmrc
shared-workspace-lockfile=true
link-workspace-packages=true
prefer-workspace-packages=true
strict-peer-dependencies=false
auto-install-peers=true
Key Features:
Rust monorepo workspace configuration.
# Cargo.toml (root)
[workspace]
members = [
"crates/core",
"crates/api",
"crates/cli",
]
exclude = ["archived/*"]
[workspace.package]
version = "1.0.0"
edition = "2021"
license = "MIT"
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.35", features = ["full"] }
Individual crate:
# crates/core/Cargo.toml
[package]
name = "my-core"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
serde.workspace = true
tokio.workspace = true
my-api = { path = "../api" }
Specify dependencies on other workspace packages.
{
"name": "@myorg/web-app",
"version": "1.0.0",
"dependencies": {
"@myorg/ui": "workspace:*",
"@myorg/utils": "workspace:^",
"@myorg/api-client": "1.2.3",
"react": "^18.2.0"
}
}
Workspace Protocol Variants:
workspace:* - Any version in workspaceworkspace:^ - Compatible version (semver caret)workspace:~ - Patch-level version (semver tilde)Configure how dependencies are hoisted to root.
{
"name": "my-monorepo",
"workspaces": {
"packages": ["packages/*"],
"nohoist": [
"**/react-native",
"**/react-native/**",
"**/@babel/**"
]
}
}
Hoisting Strategies:
Keep related dependencies in sync across packages.
{
"name": "my-monorepo",
"private": true,
"syncpack": {
"semverGroups": [
{
"range": "",
"dependencies": ["react", "react-dom"],
"packages": ["**"]
}
],
"versionGroups": [
{
"label": "React ecosystem must match",
"dependencies": ["react", "react-dom"],
"dependencyTypes": ["prod", "dev"],
"pinVersion": "18.2.0"
}
]
}
}
Use tools like syncpack to enforce consistency:
# Check for version inconsistencies
pnpm syncpack list-mismatches
# Fix version inconsistencies
pnpm syncpack fix-mismatches
# Update all versions
pnpm syncpack update
Handle peer dependencies correctly across workspace packages.
{
"name": "@myorg/ui",
"version": "1.0.0",
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
},
"devDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
Best Practices:
peerDependenciesMeta for optional peersOrganize shared code for maximum reusability.
packages/
├── ui/
│ ├── src/
│ │ ├── components/ # Reusable components
│ │ ├── hooks/ # Custom React hooks
│ │ ├── styles/ # Shared styles
│ │ └── index.ts # Public API
│ └── package.json
├── utils/
│ ├── src/
│ │ ├── string/ # String utilities
│ │ ├── date/ # Date utilities
│ │ ├── validation/ # Validation functions
│ │ └── index.ts # Public API
│ └── package.json
└── config/
├── eslint-config/
├── tsconfig/
└── prettier-config/
Shared Library Design:
Share TypeScript types efficiently.
// packages/types/src/user.ts
export interface User {
id: string;
email: string;
name: string;
role: UserRole;
}
export enum UserRole {
Admin = 'admin',
User = 'user',
Guest = 'guest',
}
export type CreateUserInput = Omit<User, 'id'>;
export type UpdateUserInput = Partial<CreateUserInput>;
// packages/types/package.json
{
"name": "@myorg/types",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./user": {
"types": "./dist/user.d.ts",
"default": "./dist/user.js"
}
}
}
Share build and tooling configuration across packages.
// packages/tsconfig/base.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
}
}
// apps/web/tsconfig.json
{
"extends": "@myorg/tsconfig/base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"],
"references": [
{ "path": "../../packages/ui" },
{ "path": "../../packages/utils" }
]
}
Understand and visualize package dependencies.
{
"name": "@myorg/web",
"dependencies": {
"@myorg/ui": "workspace:*",
"@myorg/api-client": "workspace:*"
}
}
Generate dependency graph:
# Using pnpm
pnpm list --depth 10 --json > deps.json
# Using Nx
nx graph
# Using custom script
node scripts/generate-dep-graph.js
Ensure packages build in correct dependency order.
{
"name": "my-monorepo",
"scripts": {
"build": "turbo run build",
"build:order": "pnpm -r --workspace-concurrency=1 run build"
}
}
Turbo pipeline configuration:
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
}
}
}
Dependency Resolution:
^build - Build dependencies firstdependsOn - Explicit task dependenciesPrevent and detect circular dependencies.
// scripts/check-circular-deps.js
import madge from 'madge';
async function checkCircularDeps() {
const result = await madge('src', {
fileExtensions: ['ts', 'tsx'],
detectiveOptions: {
ts: { skipTypeImports: true }
}
});
const circular = result.circular();
if (circular.length > 0) {
console.error('Circular dependencies detected:');
circular.forEach(cycle => {
console.error(cycle.join(' -> '));
});
process.exit(1);
}
}
checkCircularDeps();
Add to CI pipeline:
# .github/workflows/ci.yml
- name: Check circular dependencies
run: pnpm check:circular
Each package has its own version, released independently.
{
"name": "@myorg/ui",
"version": "2.1.0"
}
{
"name": "@myorg/utils",
"version": "1.5.3"
}
Advantages:
Use when:
All packages share the same version number.
{
"name": "@myorg/ui",
"version": "3.2.0"
}
{
"name": "@myorg/utils",
"version": "3.2.0"
}
Advantages:
Use when:
Apply semver principles to workspace packages.
Breaking Changes (Major):
New Features (Minor):
Bug Fixes (Patch):
Define explicit boundaries and responsibilities for each package.
Implementation:
index.ts)Example:
// packages/ui/src/index.ts - Clear public API
export { Button } from './components/Button';
export { Input } from './components/Input';
export type { ButtonProps, InputProps } from './types';
// Internal implementation details NOT exported
// ./components/Button/ButtonStyles.ts
// ./utils/internal-helper.ts
Reduce dependencies between packages to maintain flexibility.
Implementation:
Example:
// Loose coupling via interfaces
interface Logger {
log(message: string): void;
}
class PaymentService {
constructor(private logger: Logger) {}
processPayment(amount: number) {
this.logger.log(`Processing payment: ${amount}`);
// Implementation
}
}
Maintain consistent tooling across all packages.
Implementation:
Example:
// packages/eslint-config/index.json
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"no-console": "warn",
"@typescript-eslint/no-unused-vars": "error"
}
}
Use predictable naming patterns across packages.
Implementation:
@org/package-name)Example:
@myorg/web-app
@myorg/mobile-app
@myorg/ui-components
@myorg/api-client
@myorg/utils-date
@myorg/utils-string
@myorg/config-eslint
@myorg/config-typescript
Maintain comprehensive documentation for all packages.
Implementation:
Example:
# @myorg/ui
React component library for MyOrg applications.
## Installation
`pnpm add @myorg/ui`
## Usage
import { Button } from '@myorg/ui';
<Button onClick={handleClick}>Click me</Button>
## API Reference
See [API.md](./API.md) for detailed documentation.
Assign clear ownership and responsibility for packages.
Implementation:
Example:
# CODEOWNERS
/packages/ui/ @frontend-team
/packages/api-client/ @api-team
/packages/auth/ @security-team
/services/ @backend-team
Define and maintain clear API contracts between packages.
Implementation:
Example:
// packages/api-client/src/contracts.ts
/**
* User API contract
* @version 1.0.0
*/
export interface UserAPI {
getUser(id: string): Promise<User>;
createUser(data: CreateUserInput): Promise<User>;
updateUser(id: string, data: UpdateUserInput): Promise<User>;
deleteUser(id: string): Promise<void>;
}
Plan for package updates and breaking changes.
Implementation:
Example:
// Deprecation with migration path
/**
* @deprecated Use `getUser` instead
* This function will be removed in v3.0.0
*/
export function fetchUser(id: string): Promise<User> {
console.warn('fetchUser is deprecated, use getUser instead');
return getUser(id);
}
Maintain security isolation between packages.
Implementation:
Example:
{
"scripts": {
"security:audit": "pnpm audit --audit-level=high",
"security:check": "pnpm dlx audit-ci --high"
}
}
Ensure tests don't have unintended dependencies.
Implementation:
Example:
// packages/ui/src/__tests__/Button.test.tsx
import { render, screen } from '@testing-library/react';
import { Button } from '../Button';
// Test isolated from other packages
describe('Button', () => {
it('renders correctly', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
});
Creating excessive dependencies between packages.
Symptoms:
Solution:
Packages with overlapping or undefined purposes.
Symptoms:
Solution:
Different versions of same dependency across packages.
Symptoms:
Solution:
Not leveraging caching and incremental builds.
Symptoms:
Solution:
Inadequate documentation for packages and APIs.
Symptoms:
Solution:
Treating monorepo as single large application.
Symptoms:
Solution:
Sharing code that should remain private.
Symptoms:
Solution:
Importing from package internals instead of public API.
Symptoms:
Solution:
Lack of clear versioning approach.
Symptoms:
Solution:
Deep or circular dependency relationships.
Symptoms:
Solution:
Apply monorepo architecture principles when: