A Ascensão dos Monorepos: Arquitetura com Turborepo
Do problema concreto de duplicação de código entre repositórios ao setup completo de um monorepo com PNPM Workspaces, Turborepo, Remote Caching e pacotes compartilhados.
Você tem um painel Admin em React, um e-commerce em Next.js e uma API em Node.js. Cada um em seu próprio repositório. Parece organizado — até o dia em que você precisa atualizar o componente <Button> do design system. São três PRs, três pipelines de CI, três revisões e inevitavelmente um dos repos fica desatualizado por semanas. Esse é o problema concreto que Monorepos resolvem.
A arquitetura de Monorepo coloca todos os projetos em um único repositório Git, divididos em workspaces. O Turborepo é o orquestrador de builds que torna isso viável em escala — com cache inteligente, paralelismo e Remote Cache compartilhado entre máquinas.
Monorepo vs Polyrepo: O Tradeoff Real
A decisão entre monorepo e polyrepo não é técnica — é organizacional. Antes de avançar, entenda o contexto em que cada um faz sentido:
- Polyrepo faz sentido quando os times são completamente independentes, os produtos têm ciclos de release distintos, ou você precisa de permissões de acesso granulares por repositório.
- Monorepo faz sentido quando múltiplos produtos compartilham componentes, tipos TypeScript, configurações ou regras de negócio. Quando um bug em um pacote compartilhado precisa ser corrigido e testado em todos os consumidores ao mesmo tempo.
- Empresas que usam monorepo em produção: Google (todo código em um repo), Meta, Airbnb, Vercel (Next.js + Turbo no mesmo repo), Nx (próprio monorepo de ferramentas).
Estrutura de um Monorepo com PNPM Workspaces
O PNPM é o gerenciador de pacotes preferido para monorepos por causa do seu node_modules baseado em links simbólicos (hardlinks + symlinks), que ocupa muito menos espaço em disco do que npm ou yarn ao lidar com múltiplos workspaces. Aqui está a estrutura ideal:
meu-monorepo/
├── apps/
│ ├── web/ # Next.js — loja/site público
│ ├── admin/ # React + Vite — painel administrativo
│ └── api/ # Node.js + Express — backend
├── packages/
│ ├── ui/ # Componentes React compartilhados
│ ├── types/ # Interfaces TypeScript compartilhadas
│ ├── utils/ # Funções utilitárias (formatação, validação)
│ ├── eslint-config/ # Config do ESLint da empresa
│ └── tsconfig/ # Configs base do TypeScript
├── turbo.json # Configuração do Turborepo
├── pnpm-workspace.yaml # Define os workspaces do PNPM
└── package.json # Root — scripts e devDependencies globaispackages:
- 'apps/*'
- 'packages/*'Criando Pacotes Compartilhados
O ponto central de um monorepo é que apps/web importa de packages/ui como se fosse uma dependência npm normal. Veja como configurar o pacote @acme/ui:
{
"name": "@acme/ui",
"version": "0.0.1",
"private": true,
"exports": {
"./*": {
"import": "./src/*.tsx",
"types": "./src/*.tsx"
}
},
"peerDependencies": {
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@acme/tsconfig": "workspace:*",
"typescript": "catalog:"
}
}Agora no apps/web/package.json, instale o pacote local com a sintaxe workspace:*:
{
"name": "web",
"dependencies": {
"@acme/ui": "workspace:*",
"@acme/types": "workspace:*",
"next": "15.0.0",
"react": "^18"
}
}E no código do Next.js, simplesmente importe:
// Importa o Button do pacote compartilhado — sem duplicação
import { Button } from '@acme/ui/button';
import type { Product } from '@acme/types/product';
export default function HomePage() {
return (
<main>
<Button variant="primary">Ver produtos</Button>
</main>
);
}Use o campo "catalog" do PNPM (disponível desde v9) para definir versões de dependências uma única vez na raiz e reutilizar em todos os workspaces. Isso elimina a dessincronização de versões — o pior inimigo de monorepos grandes.
Configurando o Turborepo
O Turborepo lê o turbo.json na raiz para entender como os workspaces se relacionam e como cachear cada tarefa:
{
"$schema": "https://turbo.build/schema.json",
"ui": "tui",
"tasks": {
"build": {
// Este workspace só pode buildar depois que suas dependências buildarem
"dependsOn": ["^build"],
// Quais arquivos fazem parte da saída (para cache)
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"dev": {
"cache": false, // Dev server não deve ser cacheado
"persistent": true // Processo de longa duração (watch mode)
},
"lint": {
// Lint pode rodar em paralelo, sem dependências entre workspaces
"dependsOn": []
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
},
"typecheck": {
"dependsOn": ["^build"]
}
}
}O operador ^ em "^build" é a chave do Turborepo: significa "as dependências do grafo devem executar build antes de mim". Então se apps/web depende de @acme/ui, o Turborepo garante que packages/ui seja buildado primeiro, e só então inicia o build do apps/web.
Como o Cache do Turborepo Funciona
O Turborepo cria um hash de cada tarefa baseado em: arquivos de entrada, variáveis de ambiente relevantes, e os outputs das dependências. Se nada mudou, ele restaura o output cacheado sem executar o comando. Isso significa que um build que levava 4 minutos pode levar 200ms no segundo run.
# Primeiro run: executa tudo
pnpm turbo run build
# Saída típica:
# packages/tsconfig:build: cache miss, executing...
# packages/ui:build: cache miss, executing...
# apps/web:build: cache miss, executing...
# Total: 4m 12s
# Segundo run sem alterações:
pnpm turbo run build
# Saída típica:
# packages/tsconfig:build: cache hit, replaying output...
# packages/ui:build: cache hit, replaying output...
# apps/web:build: cache hit, replaying output...
# Total: 187ms ← 99% mais rápidoRemote Caching: Compartilhando Cache entre Máquinas
O Remote Cache é onde o Turborepo realmente brilha em equipes. Por padrão, o cache fica local (./node_modules/.cache/turbo). Com o Remote Cache habilitado, o hash de cada tarefa é sincronizado com a Vercel — então quando o CI roda um build que você já executou na sua máquina, ele simplesmente baixa o cache em vez de recomputar.
# Autenticando com a Vercel para Remote Cache
npx turbo login
npx turbo link
# A partir daí, turbo run build automaticamente usa e popula o Remote Cache.
# Você pode usar servidores self-hosted também:
# npx turbo run build --remote-cache-read-only (CI read-only)
# --api="https://seu-cache-server.com" --token="TOKEN"Alternativas self-hosted ao Remote Cache da Vercel incluem o Turborepo Remote Cache open-source (ducktape/turborepo-remote-cache no GitHub) e o Nx Cloud. Para projetos Open Source ou times pequenos, o tier gratuito da Vercel é mais do que suficiente.
Scripts Globais e Filtragem por Workspace
Um dos maiores ganhos de DX em monorepos é poder rodar comandos em subconjuntos específicos de workspaces:
# Roda o dev server apenas do apps/web
pnpm turbo run dev --filter=web
# Roda build de todos os apps (mas não packages)
pnpm turbo run build --filter='./apps/*'
# Roda lint apenas dos workspaces que sofreram alterações
# em relação à branch main (útil em PRs)
pnpm turbo run lint --filter='...[origin/main]'
# Roda build do @acme/ui e todos os seus consumidores
pnpm turbo run build --filter='...@acme/ui'
# No package.json da raiz, scripts convenientes:
# "dev": "turbo run dev",
# "build": "turbo run build",
# "lint": "turbo run lint",
# "test": "turbo run test"Integrando com GitHub Actions
Com Remote Cache habilitado, o CI fica dramaticamente mais rápido. Um workflow otimizado para monorepos:
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Fetch completo para o --filter funcionar com histórico de commits
fetch-depth: 2
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint, Test, Build
run: pnpm turbo run lint test build
env:
# Token do Remote Cache Vercel
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
# Turborepo usa isso para identificar o pipeline no Remote Cache
TURBO_REMOTE_ONLY: trueConfiguração Compartilhada de TypeScript e ESLint
Um dos maiores benefícios concretos: um único arquivo tsconfig base e uma única config do ESLint para toda a empresa.
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true
}
}{
"extends": "@acme/tsconfig/nextjs.json",
"compilerOptions": {
"plugins": [{ "name": "next" }],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}Conclusão
Monorepos com Turborepo não são complexidade gratuita — são uma resposta a problemas reais de escala. A combinação de PNPM Workspaces (espaço em disco eficiente, symlinks), Turborepo (cache de builds, paralelismo inteligente) e Remote Cache (compartilhamento entre máquinas) transforma um pipeline de CI de 10 minutos em algo sub-minuto na maioria dos PRs.
O ponto de virada para adotar um monorepo é quando você se pega abrindo mais de um repositório para implementar uma única feature. Nesse momento, o overhead de sincronização entre repos supera o overhead de configurar um monorepo. Para times pequenos ainda crescendo, vale começar com um monorepo desde o início — é muito mais fácil separar depois do que unir.