Monorepo vs Polyrepo: The Real Decision for Full-Stack JS Teams

Technical PM + Software Engineer
Monorepo vs Polyrepo: The Real Decision for Full-Stack JS Teams
================================================================
Problem
-------
Your full‑stack JavaScript team ships frontend apps, backend services, and reusable packages (UI components, shared types, auth helpers). You frequently need cross‑cutting changes: updating interfaces, bumping shared UI styles, or running end‑to‑end changes that span client + server. Coordination is painful: dozens of PRs across repositories, versioning friction, and duplicated CI work. You’ve heard the monorepo pitch — “one repo to rule them all” — but you’ve also seen big monorepos get slow and unwieldy.
Solution (short)
---------------
Choose the model that matches the coupling and lifecycle of your code. Use a monorepo (pnpm workspaces + Turborepo) when packages/apps are tightly coupled, you need atomic cross‑repo changes, and you want unified local dev and caching. Use polyrepo when services are independent (distinct release cycles, ownership, security boundaries), or when repository size and tooling diversity are problems. If you can’t decide, adopt a hybrid: monorepo for core shared packages + polyrepo for independent services.
This article gives concrete setups, examples, and decision criteria so your team can pick and implement the right model.
Quick definitions
-----------------
- Monorepo: One Git repo containing multiple packages/apps. Use pnpm workspaces to hoist and link dependencies; use Turborepo to orchestrate builds, tests, caching, and filtering.
- Polyrepo: One Git repo per package/app. Shared code is published to a package registry (npm/private registry) or consumed via Git URLs.
When a monorepo genuinely simplifies life
----------------------------------------
Choose a monorepo when one or more of these are true:
- Frequent cross‑package changes: You commonly change a shared type or component and update multiple apps in a single PR.
- Shared development speed matters: Developers run front‑and‑back dev servers simultaneously and expect changes to propagate locally.
- One team owns most code: Single ownership and aligned release cadence reduce overhead.
- You want build/test caching and pipeline orchestration: Turborepo adds incremental builds and CI caches that speed up work at scale.
- You maintain a large shared UI library or TypeScript types that must stay in sync.
Example realities solved by monorepo
- Changing a GraphQL schema and updating resolvers + front‑end queries in a single commit.
- Refactoring a UI component used by multiple apps without publishing a new version to npm for every small change.
- Running `pnpm i` once for the whole repo and linking packages for fast local iteration.
Concrete monorepo setup (pnpm + Turborepo)
-----------------------------------------
Folder layout:
- package.json (root)
- pnpm-workspace.yaml
- turbo.json
- apps/
- web/ (Next.js)
- admin/ (Next.js)
- packages/
- ui/
- api-utils/
- types/
Steps (minimal):
- Create repo and install tooling:
- pnpm init -w
- pnpm add -D turbo -w
- pnpm add -w -D typescript
- pnpm-workspace.yaml
```yaml
packages:
- 'apps/*'
- 'packages/*'
```
- root package.json (scripts)
```json
{
"name": "monorepo-root",
"private": true,
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"lint": "turbo run lint",
"test": "turbo run test"
},
"devDependencies": {
"turbo": "latest"
}
}
```
- turbo.json (pipeline)
```json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"dev": {
"cache": false
},
"test": {
"dependsOn": ["build"]
},
"lint": {}
}
}
```
- Package code: each app/package has its own package.json and scripts like "build", "dev", "test".
- TypeScript project references (optional) for faster type-checking across packages:
- packages/ui/tsconfig.json references packages/types.
- Local development: run `pnpm install`, then `pnpm dev`. Turbo will orchestrate tasks and reuse cached outputs across CI/dev.
Tips when using Turborepo + pnpm:
- Add outputs in turbo.json so Turbo can cache artifacts.
- Use `--filter` to run tasks for a subset of packages: e.g., `turbo run build --filter=./apps/web`.
- Consider remote caching (Turbo Cloud or self‑hosted) for CI speed.
When a monorepo adds overhead you don’t need
--------------------------------------------
Avoid monorepo if: