ZYVOPMulti-Platform Sync
SeriesAI NewsWhy ZyVOPJoin Discord
LoginGet Started
ZYVOPMulti-Platform Sync

The Developer Publishing Hub. Write once, publish everywhere, and make your work citation-ready with built-in SEO, AEO, and GEO discovery support. Zero reader paywalls.

Content

  • Categories
  • Tags
  • Badges
  • Leaderboard
  • Write Article
  • Newsletter

Company

  • About Us
  • Why ZyVOP
  • Changelog
  • Compare Platforms
  • Hashnode vs ZyVOP
  • DEV vs ZyVOP
  • Developer API & CLI
  • Author Handbook
  • Contact

Connect

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

© 2026 ZyVOP. Developer Publishing Hub.

Zero paywalls · Full content ownership
All systems operational
HomeMonorepo Best Practices

Monorepo Best Practices

Maksym Kuzmitskyi (MaximusFT)
Maksym Kuzmitskyi (MaximusFT)
Staff Frontend Engineer
July 30, 2026Updated August 31, 2026
4 min read
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.

Comments (1)

Login to post a comment.

ZyVOP

ZyVOP

Word WarriorEarly Bird
1 month ago

Welcome to ZyVOP

Maksym Kuzmitskyi (MaximusFT)
Maksym Kuzmitskyi (MaximusFT)

Staff Frontend Engineer

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

Subscribe to Maksym Kuzmitskyi (MaximusFT)'s Newsletter

Direct email dispatches when new stories are published. Zero algorithms.

More from Maksym Kuzmitskyi (MaximusFT)

View profile

You Only Get to See It From the Outside Once

The most valuable thing an experienced engineer brings to a new project isn't a skill — it's a vantage point. For a short window, you can still see the thing from the outside, the way a user or a newcomer does. Six months in, that view is gone. And most teams burn it in the first week without noticing.

6 minAug 11

The Command Palette Is an Architecture, Not a Widget

The theming article ended on a move from how an app looks to how power users drive it. The command palette — the Cmd+K menu that fuzzy-searches everything you c...

5 minAug 7

Code-Splitting Is a Boundary Decision, Not a Bundle Trick

The command palette article closed by promising the last piece of this look-and-feel stretch, and it's the one that looks the most like a solved problem: code-s...

6 minAug 7

You Probably Don't Need Multi-Agents

I work across six or seven repositories on one project — a big hybrid thing, part microfrontend, part backend, several apps that all talk to each other. When I ...

6 minAug 7

Analytics Is Architecture: Stop Sprinkling track() Everywhere

This cluster has been about how the parts of an app communicate — event buses for ambient signals, feature flags for toggling behavior. Both articles kept circl...

5 minJul 30