Monorepo vs Multirepo: Choosing for Client Projects
The Repository Structure Dilemma
One of the first things to decide when starting a client project is repository structure. Frontend, backend, admin panel... should these go in one repo or be managed separately?
Initially, I thought "just do whatever's convenient." But after several trial and error experiences, I've established clear criteria based on situations.
Problems with Multirepo
I Started with Multirepo
Project A
βββ project-a-frontend/ (separate repo)
βββ project-a-backend/ (separate repo)
βββ project-a-admin/ (separate repo)
I thought managing each independently would be clean. But reality was different.
Problem 1: Type Sharing Hell
Frontend and backend needed to use the same types, but with different repos, I had to copy-paste.
// project-a-backend/src/types/user.ts
interface User {
id: string
email: string
name: string
}
// project-a-frontend/src/types/user.ts
// Copy and paste... manual sync every time API spec changes
interface User {
id: string
email: string
name: string
}
Every time API specs changed, I had to modify both sides. Of course, mistakes happened, only discovered at runtime.
Problem 2: Dependency Version Mismatch
// project-a-frontend/package.json
"dependencies": {
"axios": "^1.4.0",
"date-fns": "^2.30.0"
}
// project-a-backend/package.json
"dependencies": {
"axios": "^0.27.0", // Why different?
"date-fns": "^2.28.0"
}
Over time, dependency versions diverge. Every time I applied security patches, I had to visit all repos.
Problem 3: Deployment Synchronization
Frontend and backend needed to deploy simultaneously, but with different repos, each needed separate deployment. CI/CD pipelines also managed separately.
Deploying one and forgetting the other caused bugs.
Switching to Monorepo
Structure Change
project-a/
βββ packages/
β βββ frontend/
β βββ backend/
β βββ admin/
β βββ shared/ # Shared code
βββ package.json
βββ pnpm-workspace.yaml
βββ turbo.json
Type Sharing Problem Solved
// packages/shared/src/types/user.ts
export interface User {
id: string
email: string
name: string
}
// packages/frontend/src/api/user.ts
import { User } from "@project-a/shared"
// packages/backend/src/api/user.ts
import { User } from "@project-a/shared"
Define types in one place, import from both sides. When API specs change, just modify shared. TypeScript catches type errors on both sides.
Dependency Integration
# pnpm-workspace.yaml
packages:
- packages/*
# Root package.json
"devDependencies": {
"typescript": "^5.0.0",
"eslint": "^8.0.0"
}
Common dependencies managed at root. Only individual package dependencies in each package.json.
Unified Deployment
# .github/workflows/deploy.yml
jobs:
deploy:
steps:
- name: Deploy Backend
run: pnpm --filter backend deploy
- name: Deploy Frontend
run: pnpm --filter frontend deploy
Sequential deployment in one workflow. No more accidentally deploying just one.
Monorepo Tool Selection
Why I Chose Turborepo
There are several monorepo tools:
- Nx
- Lerna
- Turborepo
- Yarn/pnpm Workspaces
I use the Turborepo + pnpm combination.
Turborepo advantages:
- Simple configuration
- Fast build caching
- Good Vercel integration
- Low learning curve
// turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/", "dist/"]
},
"dev": {
"cache": false
},
"lint": {},
"test": {}
}
}
pnpm Workspaces
# pnpm-workspace.yaml
packages:
- packages/*
- apps/*
Installing dependencies between packages:
pnpm add @project-a/shared --filter frontend
Considerations for Client Projects
Handoffs
A major advantage of monorepo is easier handoffs.
β Multirepo handoff
"Frontend is this repo, backend is that repo, admin is yet another repo.
Each has different .env settings and deployment methods."
β
Monorepo handoff
"Just clone this one repo.
After pnpm install, pnpm dev runs all services."
When handing projects to clients or successor developers, one repo means less to explain and fewer chances for mistakes.
Permission Management
But sometimes multirepo is necessary. When clients want only frontend access, or backend code needs security separation.
Situation: Collaboration with external frontend developer
β Separate frontend to its own repo
β Keep the rest as monorepo
Client Requirements
"Deploy frontend on our servers, and you operate the backend on your servers."
Such requirements mean completely different deployment environments, so multirepo might be better. But code sharing is still needed, so publishing only the shared package to npm is also an option.
My Criteria Summary
Choose Monorepo
- When I develop both frontend/backend
- When type sharing is important
- When unified deployment is needed
- When simplifying handoffs
Choose Multirepo
- When multiple developers/teams work independently
- When permission separation is needed
- When deployment environments are completely different
- When clients need access only to specific parts
Actual Project Structure Example
Currently running project structure:
dante-company/
βββ apps/
β βββ web/ # Main website
β βββ blog/ # Blog (the site you're viewing now)
β βββ admin/ # Admin page
βββ packages/
β βββ ui/ # Shared UI components
β βββ config/ # ESLint, TS configs
β βββ types/ # Shared types
βββ pnpm-workspace.yaml
βββ turbo.json
All projects share the same design system and types. When starting a new project, just add under apps/.
Conclusion
Monorepo vs multirepo isn't a matter of "which is better." There's just the appropriate choice based on situation.
When a solo developer handles client projects, monorepo is advantageous in most situations. In terms of type sharing, dependency management, and handoffs.
However, if project scale grows, team grows, or permission separation is needed, it's not too late to split into multirepo then. Splitting from monorepo to multirepo is easier than the reverse.