Um monorepo não se trata de colocar tudo em um único repositório. Trata-se de organizar código relacionado para que mudanças fluam pelo sistema de forma consistente, builds permaneçam rápidos e sua equipe pare de desperdiçar tempo com coordenação entre repositórios. Para startups SaaS, a questão do monorepo não é "devemos?" — é "como configuramos sem a taxa de complexidade que matou a experiência no emprego anterior?"
Este guia cobre a configuração prática: Turborepo como orquestrador de build, estrutura de pacotes que escala, bibliotecas de UI compartilhadas e as lições do mundo real de entregar um monorepo com 18 pacotes internos. O AeroCopilot roda exatamente nessa arquitetura, e nosso CTO refinou a abordagem ao longo de anos de trabalho com monorepos empresariais na Avenue Code, onde a coordenação entre múltiplas equipes em grandes bases de código era a realidade diária.
Por Que Monorepos Vencem para SaaS
O caso de uso SaaS para monorepos é convincente por três razões específicas:
1. Código compartilhado sem a dança de publicar no npm. Seu SaaS provavelmente tem uma aplicação web, talvez um site de marketing, um dashboard administrativo e eventualmente um app mobile ou API. Esses compartilham tipos, lógica de validação, componentes de UI e regras de negócio. Em um setup polyrepo, compartilhar significa publicar no npm, versionar e coordenar atualizações entre repos. Em um monorepo, código compartilhado é importado diretamente. Mude uma definição de tipo e todo consumidor atualiza instantaneamente.
2. Mudanças atômicas entre fronteiras. Quando você renomeia um campo no banco de dados, quer atualizar o schema, a API, os tipos do frontend e os testes em um único commit. Monorepos tornam isso natural. Polyrepos tornam isso um problema de coordenação de vários dias e múltiplos PRs.
3. Ferramentas consistentes. Uma config de ESLint. Uma config de TypeScript. Um test runner. Um pipeline de CI. Cada pacote no seu monorepo herda os mesmos padrões. Sem mais "esse repo usa Prettier 2 e aquele usa Prettier 3".
Turborepo: A Ferramenta Certa para Startups
O Turborepo venceu a corrida de ferramentas de build para monorepos em startups porque faz uma coisa bem: torna npm scripts rápidos e cacheáveis sem exigir que você aprenda um novo sistema de build.
Compare as alternativas:
- Nx é poderoso mas complexo. Brilha para grandes organizações com 50+ engenheiros. Para uma startup com 2-5 desenvolvedores, o overhead de configuração não se justifica
- Lerna está efetivamente descontinuado como ferramenta standalone
- Bazel é para problemas na escala do Google. Se você está lendo este artigo, não tem problemas na escala do Google
- Turborepo adiciona cache, paralelização e execução de tarefas com consciência de dependências sobre seus scripts package.json existentes. Configuração leva 30 minutos
Instalação:
pnpm add turbo -D -w
turbo.json básico:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {},
"test": {
"dependsOn": ["^build"]
},
"typecheck": {
"dependsOn": ["^build"]
}
}
}
A sintaxe dependsOn: ["^build"] significa "faça build das minhas dependências antes de fazer meu build." O Turborepo resolve o grafo de dependências e executa tarefas na ordem correta, paralelizando onde possível. Cache remoto significa que seu CI não reconstrói pacotes inalterados.
Estrutura de Pacotes Que Escala
Aqui está a estrutura de pacotes que usamos em produção. O AeroCopilot roda 18 pacotes seguindo este padrão:
├── apps/ │ ├── web/ # Aplicação Next.js principal │ └── e2e/ # Testes E2E com Playwright ├── packages/ │ ├── database/ # Schema Prisma, migrações, client │ ├── ui/ # Componentes React compartilhados │ ├── action-middleware/ # Middleware para server actions │ ├── rbac/ # Controle de acesso baseado em papéis │ ├── mailers/ # Envio de e-mails │ ├── analytics/ # Integração de analytics │ ├── storage/ # Abstração de armazenamento de arquivos │ ├── policies/ # Políticas de autorização │ └── otp/ # Funcionalidade OTP/2FA ├── turbo.json ├── package.json └── pnpm-workspace.yaml
Princípios-chave:
- Apps consomem pacotes, nunca o inverso. O grafo de dependências flui em uma direção. Um pacote em
packages/nunca deve importar deapps/web - Cada pacote tem uma única responsabilidade. O pacote
databaseé dono do schema e do Prisma client. O pacoteuié dono dos componentes. Nenhum pacote faz duas coisas - Pacotes exportam através de barrel files. Todo pacote tem um
index.tsque re-exporta sua API pública. Detalhes de implementação interna permanecem internos - Pacotes são testáveis independentemente. Cada pacote tem sua própria suíte de testes que roda sem precisar do contexto completo da aplicação
UI Compartilhada: O Pacote de Maior ROI
O pacote de UI compartilhada é tipicamente o primeiro pacote que você extrai e o que entrega mais valor ao longo do tempo. Garante consistência visual em toda superfície do seu produto.
O que pertence ao pacote UI:
- Componentes primitivos: Button, Input, Select, Dialog, Card
- Componentes de layout: Container, Stack, Grid
- Componentes de feedback: Toast, Alert, Skeleton
- Componentes de tipografia: Heading, Text, Label
- Componentes utilitários: VisuallyHidden, Portal, Slot
O que não pertence:
- Componentes com lógica de negócio (use componentes específicos de feature no app)
- Chamadas de API ou data fetching
- Lógica de roteamento
- Estado de autenticação
Um pacote UI bem estruturado se parece com isso:
packages/ui/ ├── src/ │ ├── components/ │ │ ├── button/ │ │ │ ├── button.tsx │ │ │ ├── button.test.tsx │ │ │ └── index.ts │ │ ├── input/ │ │ └── dialog/ │ ├── styles/ │ │ └── globals.css │ └── index.ts ├── package.json └── tsconfig.json
Usamos Tailwind CSS 4 com componentes headless Base UI como fundação. Isso nos dá primitivos sem estilo e acessíveis que estilizamos com classes utilitárias do Tailwind. O resultado são componentes visualmente consistentes, acessíveis por padrão e trivialmente customizáveis.
O Padrão do Pacote Database
O pacote database é o segundo pacote compartilhado mais crítico. Ele é dono do seu schema Prisma, migrações, dados seed e o Prisma client gerado.
packages/database/ ├── prisma/ │ ├── schema.prisma │ ├── migrations/ │ └── seed.ts ├── src/ │ ├── client.ts # Singleton do Prisma client │ ├── queries/ # Funções de query reutilizáveis │ └── index.ts ├── package.json └── tsconfig.json
Por que isso importa: Quando seu schema de banco de dados está em um pacote compartilhado, todo app e pacote que precisa de acesso ao banco importa da mesma fonte. Type safety flui do schema através do Prisma client para todo consumidor. Mude um nome de campo no schema e erros TypeScript aparecem em todo lugar que aquele campo é referenciado — no mesmo passo de build.
A camada de funções de query é opcional mas valiosa. Em vez de escrever queries Prisma brutas no código do seu app, você encapsula queries comuns no pacote database. Isso cria uma camada de acesso a dados limpa que pode ser testada independentemente e reutilizada entre apps.
CI/CD para Monorepos
CI de monorepo requer uma capacidade crítica: executar tarefas apenas para pacotes que mudaram. Sem isso, seu tempo de CI cresce linearmente com o número de pacotes.
O Turborepo lida com isso através da flag --filter:
# Build apenas pacotes afetados por mudanças desde main turbo build --filter=...[main] # Testar apenas o pacote database e seus dependentes turbo test --filter=database...
Um workflow prático de GitHub Actions:
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm turbo build lint test typecheck --filter=...[origin/main]
O fetch-depth: 0 é essencial — o Turborepo precisa do histórico git completo para determinar quais pacotes mudaram. A flag --filter=...[origin/main] restringe a execução apenas aos pacotes afetados.
Cache remoto via Vercel ou cache self-hosted reduz ainda mais o tempo de CI. Em um monorepo típico com 10-18 pacotes, cache remoto corta o CI de 8 minutos para menos de 2 minutos em mudanças incrementais.
Erros Comuns e Como Evitá-los
Erro 1: Dependências circulares. Pacote A importa do Pacote B que importa do Pacote A. O Turborepo vai detectar isso no build, mas é melhor prevenir arquiteturalmente. Use dependency-cruiser ou madge para visualizar e enforçar seu grafo de dependências.
Erro 2: Vazar tipos internos. Cada pacote deve ter uma API pública clara definida em seu barrel export. Não exporte tipos de implementação interna dos quais consumidores não devem depender.
Erro 3: Scripts inconsistentes no package.json. Todo pacote deve ter os mesmos nomes de script: build, dev, lint, test, typecheck. O Turborepo executa tarefas por nome em todo o workspace. Se um pacote chama seu script de lint de check em vez de lint, ele é ignorado silenciosamente.
Erro 4: Não fixar versões de dependências. Use pnpm com sua resolução estrita de dependências. Adicione um .npmrc com strict-peer-dependencies=true. Isso previne os problemas de dependências fantasma que assolam monorepos com resolução permissiva. Discutimos decisões arquiteturais relacionadas que afetam a manutenibilidade a longo prazo.
Erro 5: Extração prematura de pacotes. Nem tudo precisa ser um pacote separado no primeiro dia. Comece com apps/web e extraia para packages/ apenas quando tiver um caso claro de reuso ou uma fronteira clara. Extração excessiva cria overhead de coordenação sem benefício correspondente.
Quando Começar Com um Monorepo
Comece com um monorepo se alguma dessas condições for verdadeira:
- Você está construindo um SaaS com mais de uma superfície voltada ao usuário (app web + site de marketing + dashboard admin)
- Você tem lógica de negócio compartilhada entre frontend e backend
- Você planeja construir uma biblioteca de componentes para consistência
- Você quer deploys atômicos onde mudanças relacionadas são enviadas juntas
Não comece com um monorepo se:
- Você está construindo uma aplicação única e standalone sem necessidade de código compartilhado
- Sua equipe tem zero experiência com ferramentas de monorepo e está com prazo apertado
- Seu produto está em modo de exploração pura e a base de código muda radicalmente toda semana
Para a maioria das startups SaaS após a fase inicial de protótipo, o monorepo se paga no primeiro mês. O custo de configuração é de 2-4 horas com o Turborepo. O benefício contínuo é medido em horas-desenvolvedor economizadas por semana em coordenação, consistência e mudanças transversais.
Comece com Turborepo, pnpm workspaces e três pacotes: seu app, uma biblioteca de UI compartilhada e um pacote database. Cresça a partir daí conforme seu produto e equipe escalam.
