All posts
5 min read

Monorepo or Separate Repos? A Practical Guide for Small Teams

Should your frontend, backend, and shared code live in one repository or several? The real trade-offs — atomic changes, tooling cost, deploy independence, access control — how monorepo tooling like workspaces and Turborepo helps, and why AI agents tip the balance.

architecturetoolingdeveloper experienceintermediate

Once an app grows beyond one piece — a web frontend, an API, a mobile app, a shared library of types, a background worker — you face a structural decision: keep everything in one repository (a monorepo), or give each piece its own repository (a polyrepo)?

Both work. Large companies famously succeed with each. For small teams, though, the trade-offs are more specific — and AI coding agents shift them noticeably.

What each looks like

Monorepo:

my-product/
├── apps/
│   ├── web/          # Next.js frontend
│   ├── api/          # backend service
│   └── worker/       # background jobs
├── packages/
│   ├── db/           # schema, migrations, generated types
│   ├── ui/           # shared components
│   └── config/       # shared lint/TS config
└── package.json      # workspace root

Polyrepo: my-product-web, my-product-api, my-product-worker, and my-product-shared as four separate repositories, with shared code published as a package.

The case for a monorepo

Atomic changes across boundaries. Add a field to the API and update the frontend that uses it, in one commit and one pull request. Reviewed together, tested together, deployed consistently. In a polyrepo, the same change is two or three coordinated PRs merged in the right order.

Shared code without publishing. The frontend imports types from packages/db directly. No versioning a shared package, no publishing it, no "which version of the shared lib is the API on?"

One set of tooling. One lint config, one TypeScript config, one CI setup, one place to update dependencies.

Easier refactoring. Rename a function and every caller across the whole product is right there to update — and to verify with one type check.

Better for AI agents. This is the newer argument, and it's significant. An agent working in a monorepo can see the whole system: the API contract and its consumers, the schema and every query. It can make a cross-cutting change end to end and verify it with one type check and one test run. Split across repos, the agent only sees its slice, and cross-repo consistency falls back on you. (What Is an AI Coding Agent?)

The case for separate repos

Independent deploys and ownership. Each repo has its own release cadence, CI, and owners. When separate teams own separate services with a stable contract between them, separate repos reflect reality.

Access control. Repos are the natural permission boundary on GitHub. If contractors should see the mobile app but not the billing service, separate repos make that simple.

Simpler tooling at the start. A single-app repo needs no workspace configuration or task orchestration.

Scale limits. Very large monorepos need serious tooling to keep CI and git operations fast. (Small teams are nowhere near this.)

Different stacks. A Python ML service and a TypeScript web app share little tooling; a monorepo adds friction without much benefit.

A practical rule for small teams

If the pieces are the same product, change together, and share types, use a monorepo. Frontend + API + worker + shared schema for one product is the classic case.

If the pieces are genuinely independent — different products, different teams, different stacks, a stable contract between them, or a hard access boundary — separate repos are fine.

For most small teams building one product, that means a monorepo.

Making a monorepo work: the tooling

Workspaces

npm, pnpm, and Yarn all support workspaces: several packages in one repo, installed together, able to import each other.

// package.json (root)
{
  "private": true,
  "workspaces": ["apps/*", "packages/*"]
}

pnpm (with pnpm-workspace.yaml) is a popular choice for monorepos for its speed and strictness about undeclared dependencies.

A package imports another by name:

import { OrderSchema } from '@acme/db'

Task orchestration

With several apps, "run the build" means building packages in the right order and not rebuilding what hasn't changed. Turborepo and Nx handle this: they understand the dependency graph, run tasks in parallel, and cache results so unchanged packages are skipped — locally and in CI.

turbo run build test lint

CI that only does what's needed

Configure CI to run checks only for packages affected by a change (both Turborepo and Nx can compute this). A frontend copy change shouldn't run the worker's integration tests. (Set Up CI With GitHub Actions.)

Independent deploys from one repo

A monorepo doesn't mean deploying everything together. Each app can have its own deploy pipeline, triggered when its files (or its dependencies) change. You get atomic changes without forcing atomic deploys.

Pitfalls

  • Circular dependencies between packages. packages/ui importing from apps/web is a sign of blurred boundaries. Apps depend on packages, not the other way round.
  • One giant utils package. It becomes a junk drawer every app depends on. Prefer focused packages.
  • Deploying on every change to anything. Scope deploys to what changed.
  • Leaking server code into the client. A shared package imported by both frontend and backend must not contain secrets or server-only modules. Keep server-only code in clearly named packages. (Frontend vs Backend.)

Helping agents in a monorepo

  • A root CLAUDE.md describing the layout — which app is which, where shared types live, how to run each app's checks.
  • Nested CLAUDE.md files in each app for app-specific rules; Claude Code loads the relevant ones as it works in those directories. (How to Write a CLAUDE.md.)
  • One command to verify everything (turbo run typecheck test), so an agent's cross-cutting change can be checked in one go.

The summary

Monorepo Separate repos
Cross-cutting change One PR Coordinated PRs
Shared code Direct import Published package
Tooling Workspaces + Turborepo/Nx Simple per repo
Access control Coarser Per repo
AI agent visibility Whole system One slice
Best for One product, shared types Independent services/teams

EasySpawn runs Claude Code with your whole repository — every app and package — in one persistent workspace, so cross-cutting changes can be made and verified end to end. See how it works or join the waitlist.

Related: What Is a Tech Stack? · Why TypeScript Makes AI-Generated Code Safer · Monolith vs Microservices

Keep reading