DEV Community

Maksym Kuzmitskyi (MaximusFT)
Maksym Kuzmitskyi (MaximusFT)

Posted on • Originally published at ma-x.im on

Monorepo Best Practices

Monorepo Best Practices

I learned to love monorepos the hard way — by suffering through the alternative.

At a large Canadian automotive company, we had a repository for basic UI components, a separate repository for complex components that depended on them, and then six separate repositories — one for each product line: cars, motorcycles, engines, marine, power equipment, and more. Plus utility repos for shared configs, linting rules, and deployment scripts.

When you released a patch to the base components, the chain reaction began. Update base components, then update complex components, then update all six product repos. Something always broke somewhere in that chain. A CI job would hang. A version mismatch would surface. A team using an older version would suddenly discover their build was broken. We outsourced CI/CD to external contractors, which added another layer of friction — every issue turned into a cross-team investigation.

We even started preparing a plan to consolidate everything into a single monorepo. I left the company before they finished.

That experience made me a firm believer: if your projects share code, put them together.

Why Monorepo?

Benefits:

  • Shared code — one source of truth for components, utils
  • Atomic changes — update library and consumers in one PR
  • Better refactoring — see all usages across projects
  • Unified tooling — one config for linting, testing, building
  • Easier onboarding — everything in one repo

Drawbacks:

  • Slower CI if not optimized
  • More complex tooling needed
  • Harder access control (everything visible)

For most teams, benefits outweigh drawbacks. And for teams that have lived through multi-repo dependency hell — it's not even a question.

Tools

I use Turborepo. It's fast, simple, and works great with npm/pnpm.

Alternatives:

  • Nx - More features, steeper learning curve
  • Lerna - Older, less maintained
  • Rush - Microsoft's tool, great for large teams

Structure

Clean structure is critical:

my-monorepo/
├── apps/
│ ├── web/ # Main web app
│ ├── admin/ # Admin dashboard
│ └── mobile/ # React Native app
├── packages/
│ ├── ui/ # Shared component library
│ ├── utils/ # Shared utilities
│ ├── config/ # Shared configs (eslint, ts)
│ └── types/ # Shared TypeScript types
├── tooling/
│ ├── eslint-config/
│ └── typescript-config/
├── package.json
├── turbo.json
└── pnpm-workspace.yaml

Enter fullscreen mode Exit fullscreen mode

Apps = Deployable applications Packages = Shared libraries Tooling = Dev tools and configs

Package Setup

Each package has its own package.json:

{
  "name": "@repo/ui",
  "version": "0.0.0",
  "private": true,
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "exports": {
    ".": "./src/index.ts",
    "./button": "./src/button.tsx"
  }
}

Enter fullscreen mode Exit fullscreen mode

Apps import packages:

import { Button } from "@repo/ui";
import { formatDate } from "@repo/utils";

Enter fullscreen mode Exit fullscreen mode

TypeScript resolves these via workspace protocol.

Turborepo Configuration

turbo.json:

{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/ **", ".next/**"]
    },
    "test": {
      "dependsOn": ["^build"]
    },
    "lint": {},
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Key features:

  • dependsOn - Build dependencies first
  • outputs - Cache these directories
  • cache: false - Don't cache dev server

Workspace Protocol

Use pnpm for best monorepo support:

pnpm-workspace.yaml:

packages:
  - "apps/*"
  - "packages/*"
  - "tooling/*"

Enter fullscreen mode Exit fullscreen mode

Install workspace dependencies:

{
  "dependencies": {
    "@repo/ui": "workspace:*",
    "@repo/utils": "workspace:*"
  }
}

Enter fullscreen mode Exit fullscreen mode

Shared Configuration

Share ESLint config:

// tooling/eslint-config/index.js
module.exports = {
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react/recommended",
  ],
  // ... rules
};

Enter fullscreen mode Exit fullscreen mode

Use in packages:

{
  "eslintConfig": {
    "extends": ["@repo/eslint-config"]
  }
}

Enter fullscreen mode Exit fullscreen mode

Same pattern for TypeScript, Prettier, Jest configs.

CI/CD Optimization

Problem: CI runs all tests on every commit, even if code didn't change.

Solution: Turborepo's remote caching.

# Only rebuild what changed
turbo run build --filter=[HEAD^1]

# Use remote cache
turbo run build --cache-dir=.turbo/cache

Enter fullscreen mode Exit fullscreen mode

GitHub Actions example:

name: CI

on: [pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - uses: pnpm/action-setup@v2
        with:
          version: 8

      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: 'pnpm'

      - run: pnpm install

      - name: Build
        run: pnpm turbo run build --filter=[HEAD^1]

      - name: Test
        run: pnpm turbo run test --filter=[HEAD^1]

Enter fullscreen mode Exit fullscreen mode

Huge time savings on large PRs.

Versioning Strategy

For internal packages (not published to npm):

{
  "version": "0.0.0",
  "private": true
}

Enter fullscreen mode Exit fullscreen mode

For published packages, use Changesets :

pnpm add -DW @changesets/cli
pnpm changeset init

Enter fullscreen mode Exit fullscreen mode

Create changeset:

pnpm changeset

Enter fullscreen mode Exit fullscreen mode

It asks:

  • Which packages changed?
  • Semver bump (major/minor/patch)?
  • Changelog entry?

Then in CI:

pnpm changeset version
pnpm changeset publish

Enter fullscreen mode Exit fullscreen mode

Automatic versioning and publishing.

Common Pitfalls

1. Not Using Task Dependencies

{
  "tasks": {
    "build": {
      "dependsOn": ["^build"] // Build deps first!
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Without this, apps might build before their dependencies.

2. Circular Dependencies

Never create circular dependencies:

❌ @repo/ui -> @repo/utils -> @repo/ui

Enter fullscreen mode Exit fullscreen mode

Keep dependency graph acyclic.

3. Too Many Small Packages

Don't create a package for every util function. Group related code:

✅ @repo/utils (contains formatDate, parseUrl, etc.)
❌ @repo/format-date, @repo/parse-url, ...

Enter fullscreen mode Exit fullscreen mode

4. Ignoring Cache Configuration

Configure outputs properly:

{
  "tasks": {
    "build": {
      "outputs": ["dist/ **", "build/**", ".next/**"]
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Missing this = no caching benefits.

Testing in Monorepo

Shared Jest config:

// packages/jest-config/jest.config.js
module.exports = {
  preset: "ts-jest",
  testEnvironment: "jsdom",
  moduleNameMapper: {
    "^@repo/(.*)$": "<rootDir>/../$1/src",
  },
};

Enter fullscreen mode Exit fullscreen mode

Run all tests:

turbo run test

Enter fullscreen mode Exit fullscreen mode

Run tests for specific package:

turbo run test --filter=@repo/ui

Enter fullscreen mode Exit fullscreen mode

Development Workflow

Start all apps in dev mode:

turbo run dev --parallel

Enter fullscreen mode Exit fullscreen mode

Work on specific app:

turbo run dev --filter=web

Enter fullscreen mode Exit fullscreen mode

Watch mode for package:

turbo run dev --filter=@repo/ui

Enter fullscreen mode Exit fullscreen mode

Migration Strategy

Moving to monorepo? Do it gradually:

  1. Setup monorepo structure
  2. Move shared code to packages
  3. Migrate one app at a time
  4. Update CI/CD incrementally
  5. Train team on new workflows

Don't do big-bang migrations. Too risky.

Conclusion

Every time I set up a monorepo from scratch, I think about those six separate repos and the hours we spent chasing version mismatches across them. The initial monorepo setup takes effort — no question. But the alternative is worse: scattered code, broken dependency chains, and a CI pipeline that nobody fully understands.

Get the structure right. Invest in proper tooling. Cache aggressively. And keep your dependency graph clean. Future you will be grateful.


Questions about monorepos? Reach out.

Top comments (0)