ZyVOP Logo
Content That Connects
SeriesAI NewsCategoriesWrite for Us
ZyVOP Logo
Content That Connects

Empowering developers and creators with cutting-edge insights, comprehensive tutorials, and innovative solutions for the digital future.

Content

  • Tags
  • Badges
  • Write Article
  • Newsletter

Company

  • About Us
  • Write for Us
  • Contact

Connect

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • DMCA Policy
  • Code of Conduct

© 2026 ZyVOP. Crafted with care for the developer community.

Made with ❤️ by the ZyVOP team
All systems operational
HomeMonorepo Best Practices

Monorepo Best Practices

Maksym Kuzmitskyi (MaximusFT)
Maksym Kuzmitskyi (MaximusFT)Staff Frontend Engineer
July 30, 2026
4 min read
3 views
Monorepo Best Practices
#Monorepo#Architecture#DevOps
👍1

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

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"
  }
}

Apps import packages:

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

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
    }
  }
}

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/*"

Install workspace dependencies:

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

Shared Configuration

Share ESLint config:

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

Use in packages:

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

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

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]

Huge time savings on large PRs.

Versioning Strategy

For internal packages (not published to npm):

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

For published packages, use Changesets:

pnpm add -DW @changesets/cli
pnpm changeset init

Create changeset:

pnpm changeset

It asks:

  • Which packages changed?

  • Semver bump (major/minor/patch)?

  • Changelog entry?

Then in CI:

pnpm changeset version
pnpm changeset publish

Automatic versioning and publishing.

Common Pitfalls

1. Not Using Task Dependencies

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

Without this, apps might build before their dependencies.

2. Circular Dependencies

Never create circular dependencies:

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

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, ...

4. Ignoring Cache Configuration

Configure outputs properly:

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

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",
  },
};

Run all tests:

turbo run test

Run tests for specific package:

turbo run test --filter=@repo/ui

Development Workflow

Start all apps in dev mode:

turbo run dev --parallel

Work on specific app:

turbo run dev --filter=web

Watch mode for package:

turbo run dev --filter=@repo/ui

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.

Maksym Kuzmitskyi (MaximusFT)

Maksym Kuzmitskyi (MaximusFT)

Staff Frontend Engineer

Passionate developer sharing knowledge about modern web technologies and best practices.

Comments (0)

Login to post a comment.

Related Posts

The Alpine Mirage: How Upgrading Python Broke My Build and Led to a Truer Security Posture

A deep dive into how upgrading a Python Docker image to Alpine 3.15 caused cascading build failures across C/Rust dependencies—and why switching to Debian-slim with non-root privileges resulted in a faster, truly secure container architecture.

Read article

Service Discovery with Eureka and Spring Cloud: A Production Hands-On Guide

Learn how to build production-grade service discovery with Eureka and Spring Cloud LoadBalancer. Includes real-world heartbeat tuning and memory optimization tips.

Read article

Complex Forms Done Right

At Motorola Solutions I built a multi-step contract wizard where users selected products, services, additional options, and pricing tiers — dozens of fields tha...

Read article

Building Design Systems That Scale

At Motorola Solutions I walked into a codebase with hundreds of internal projects. Every team followed the brand book — in theory. In practice, it was chaos. Bu...

Read article

Next.js Static Export to GitHub Pages: A Production Setup Guide

Static export remains the simplest deployment model for content-focused sites. This article documents the setup behind this very site — ma-x.im — a Next.js 15 p...

Read article