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.
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/uiimporting fromapps/webis a sign of blurred boundaries. Apps depend on packages, not the other way round. - One giant
utilspackage. 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.mddescribing the layout — which app is which, where shared types live, how to run each app's checks. - Nested
CLAUDE.mdfiles 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
Getting AI to Write Tests That Actually Catch Bugs
Ask an AI for tests and you'll get plenty: tests that mock everything, assert nothing useful, and pass no matter what the code does. How to get tests that fail when behaviour breaks — what to test, how to prompt, how to check a test is real, and how tests become the agent's safety net.
Validating Input With Zod: One Schema for Forms, APIs, and Types
Every trust boundary — request bodies, query strings, webhooks, environment variables, AI output — needs runtime validation TypeScript can't provide. Using Zod schemas at each boundary, sharing them between client and server, stripping unknown keys, and useful errors.