Entendendo Monorepos: O Problema que Você Precisa Resolver
Um monorepo é um repositório único que contém múltiplos projetos, bibliotecas ou pacotes relacionados. Diferente de um multirepo (vários repositórios), um monorepo centraliza o controle de versão, facilitando o compartilhamento de código, sincronização de dependências e coordenação entre equipes. No entanto, a complexidade surge quando você precisa decidir como organizar esses projetos dentro do repositório sem criar acoplamentos desnecessários.
O desafio real não está em ter vários projetos no mesmo repositório — está em mantê-los independentes, versionados corretamente e com pipelines de CI/CD eficientes. Quando você trabalha com monorepos, precisa resolver: Como isolar mudanças? Como versionar pacotes independentemente? Como evitar que uma mudança acidental em um projeto quebre todos os outros? É aqui que Git Subtrees, Git Submodules e ferramentas como Nx entram em cena, cada uma com uma filosofia e caso de uso diferente.
Git Subtrees: Simplicidade com Custo de Flexibilidade
O Conceito Fundamental
Git Subtrees permite que você incorpore um repositório dentro de outro como um diretório, mantendo o histórico do repositório original. Diferente de Submodules, Subtrees fazem uma fusão (merge) real do código, não apenas uma referência. O histórico do projeto externo fica completamente integrado ao seu repositório principal, o que significa que você pode fazer checkout de qualquer versão anterior sem dependências externas.
A vantagem principal é a simplicidade: depois que o subtree está configurado, o desenvolvedor trabalha normalmente com o repositório. Não há necessidade de comandos especiais na maioria das operações. A desvantagem é que o histórico se torna mais complexo e as operações de sincronização (push/pull) requerem sintaxe específica.
Prática: Criando e Atualizando um Subtree
Vamos supor que você tem um repositório principal chamado meu-app e quer incorporar uma biblioteca chamada utils-lib. Primeiro, você adiciona o subtree:
git subtree add --prefix=libs/utils https://github.com/seu-usuario/utils-lib.git main
Esse comando:
1. Clona o repositório utils-lib em libs/utils
2. Integra todo o histórico no seu repositório
3. Cria um commit automático
Para fazer alterações localmente e sincronizar com o repositório externo, você edita os arquivos normalmente:
# Edite os arquivos em libs/utils/
echo "export function sum(a, b) { return a + b; }" > libs/utils/src/math.ts
# Commit normalmente
git add libs/utils/
git commit -m "Adiciona função sum ao utils"
# Push para o repositório externo (subtree push)
git subtree push --prefix=libs/utils https://github.com/seu-usuario/utils-lib.git main
Para atualizar o subtree quando há mudanças no repositório original:
git subtree pull --prefix=libs/utils https://github.com/seu-usuario/utils-lib.git main --squash
A flag --squash compacta todos os commits do repositório externo em um único commit, mantendo o histórico mais limpo. Sem ela, você verá todo o histórico de desenvolvimento do projeto externo.
Quando Usar Subtrees
Use Subtrees quando:
- Você precisa de um componente externo com atualizações ocasionais
- A equipe é pequena e a simplicidade é prioridade
- Você quer que o código externo seja facilmente modificável localmente
- Não há necessidade de versionar a biblioteca externa independentemente no monorepo
Evite Subtrees quando trabalhar com muitos projetos interdependentes ou quando precisar de atualizações frequentes e bidireccionais.
Git Submodules: Referências Reutilizáveis com Complexidade
Arquitetura e Funcionamento
Git Submodules é fundamentalmente diferente de Subtrees. Submodules cria uma referência para um commit específico de outro repositório, não uma incorporação. Seu repositório principal apenas "aponta" para uma versão exata de um repositório externo. Isso permite que múltiplos projetos reutilizem a mesma biblioteca, sempre com versões garantidas.
O grande benefício é que alterações em um Submodule não afetam automaticamente o repositório principal — você controla precisamente quando fazer upgrade. A desvantagem é que Submodules requerem passos adicionais: clonar o repositório, atualizar submodules, e gerenciar referências de commits.
Prática: Configurando e Atualizando Submodules
Adicionar um Submodule:
git submodule add https://github.com/seu-usuario/utils-lib.git libs/utils
Isso cria um arquivo .gitmodules que registra a configuração:
[submodule "libs/utils"]
path = libs/utils
url = https://github.com/seu-usuario/utils-lib.git
Quando outra pessoa clonar seu repositório, precisa inicializar os submodules:
git clone https://github.com/seu-usuario/meu-app.git
cd meu-app
git submodule init
git submodule update
Ou, em uma única linha:
git clone --recurse-submodules https://github.com/seu-usuario/meu-app.git
Para atualizar um Submodule para a versão mais recente:
cd libs/utils
git fetch origin
git checkout origin/main # Ou a branch que você quer
cd ../..
git add libs/utils
git commit -m "Atualiza utils-lib para a versão mais recente"
Se você quer fazer mudanças dentro de um Submodule, precisa estar em uma branch real (não em detached head):
cd libs/utils
git checkout main # Ou a branch padrão
echo "export function multiply(a, b) { return a * b; }" >> src/math.ts
git add src/math.ts
git commit -m "Adiciona função multiply"
git push origin main
cd ../..
git add libs/utils
git commit -m "Atualiza referência de utils-lib"
Quando Usar Submodules
Use Submodules quando:
- Você tem bibliotecas compartilhadas entre múltiplos projetos principais
- Cada projeto precisa de versões específicas de dependências
- Você quer manter históricos completamente separados
- As atualizações devem ser conscientes e deliberadas
Evite Submodules quando a maioria das mudanças é bidirecional ou quando sua equipe não está confortável com a complexidade.
Nx: A Solução Moderna para Monorepos em Escala
Filosofia e Arquitetura do Nx
Nx é um framework de build system e gerenciamento de monorepos que resolve o problema diferente: em vez de se preocupar com como Git organiza o código, Nx foca em como executar, testar e fazer deploy de projetos dentro do repositório. Nx não substitui Git — ele funciona com Git para entender dependências entre projetos e otimizar builds.
A ideia central é o grafo de dependências (Dependency Graph). Quando você altera um arquivo, Nx calcula quais projetos são afetados e executa apenas os testes, builds e lints necessários. Em um monorepo com 50 aplicações, isso economiza minutos em cada pipeline CI/CD. Além disso, Nx fornece templates de projeto, código compartilhado estruturado e convenções que garantem consistência.
Prática: Criando um Monorepo com Nx
Instale Nx globalmente ou use a versão que vem com create-nx-workspace:
npx create-nx-workspace@latest meu-workspace --preset=empty
cd meu-workspace
Agora crie duas aplicações: uma API e uma biblioteca compartilhada:
nx generate @nx/node:app api --directory=apps/api
nx generate @nx/react:app dashboard --directory=apps/dashboard
nx generate @nx/js:lib shared-utils --directory=libs/shared-utils
Seu workspace agora tem essa estrutura:
meu-workspace/
├── apps/
│ ├── api/
│ │ ├── src/
│ │ ├── project.json
│ │ └── ...
│ └── dashboard/
├── libs/
│ └── shared-utils/
│ ├── src/
│ │ └── lib/
│ │ └── utils.ts
│ └── project.json
├── nx.json
└── package.json
Vamos criar uma função na biblioteca compartilhada:
// libs/shared-utils/src/lib/utils.ts
export function formatCurrency(value: number): string {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL'
}).format(value);
}
export function calculateTax(amount: number, rate: number): number {
return amount * (rate / 100);
}
Agora a API e o dashboard podem usar essas funções. No Nx, para importar de outra biblioteca, use paths configurados no tsconfig.base.json:
// apps/api/src/main.ts
import { calculateTax } from '@meu-workspace/shared-utils';
const price = 100;
const taxValue = calculateTax(price, 15);
console.log(`Imposto: R$ ${taxValue}`);
// apps/dashboard/src/App.tsx
import { formatCurrency } from '@meu-workspace/shared-utils';
export function App() {
return <div>Preço: {formatCurrency(999.99)}</div>;
}
Quando você altera um arquivo em shared-utils e quer saber o impacto:
nx graph
Isso abre um visualizador interativo mostrando como api e dashboard dependem de shared-utils. Agora, para compilar apenas o que foi afetado:
nx affected:build
Ou execute testes apenas dos projetos afetados:
nx affected:test
Cacheing e Performance
Uma das maiores vantagens do Nx é o cache distribuído. Se você compilar a API e depois voltar para a main branch, compilar novamente é instantâneo porque Nx armazena em cache:
nx build api # Compila normalmente
git checkout main # Muda de branch
git checkout feature/nova-api # Volta à branch anterior
nx build api # Retorna do cache em milissegundos!
Você pode usar cache remoto (GitHub Actions, GitLab CI, ou Nx Cloud) para que diferentes máquinas compartilhem o cache:
nx connect-to-nx-cloud
Após isso, toda máquina na sua equipe e no CI se beneficia do cache compartilhado.
Quando Usar Nx
Use Nx quando:
- Você tem um monorepo com 5+ projetos relacionados
- CI/CD é lento por causa de builds desnecessários
- Você precisa atualizar código compartilhado frequentemente
- A equipe quer padronização e geração automática de código
- Você trabalha com diferentes tecnologias (Node, React, Angular, Vue) no mesmo repositório
Nx é overkill para repositórios com apenas 2-3 projetos simples ou para monorepos onde a maioria dos projetos é completamente independente.
Comparação Prática: Escolhendo a Estratégia Correta
Considere este cenário: você tem uma startup com três projetos: API em Node, frontend em React e uma CLI em Node. Todos compartilham funções utilitárias.
Com Subtrees: Você teria um repositório principal e adicionaria a biblioteca utils como subtree. Rápido de configurar, mas atualizações são manuais e o histórico fica complexo.
Com Submodules: Cada desenvolvedor precisaria fazer git submodule update frequentemente. A CI/CD fica mais complexa e onboarding é mais lento.
Com Nx: Você define dependências uma vez, escreve código compartilhado em libs/shared-utils, e Nx automaticamente entende que alterar uma função ali impacta API, frontend e CLI. Tests e builds são otimizados. Onboarding é simples: npm install && nx serve api.
A escolha depende da escala:
- 1-2 projetos pequenos: Submodules é suficiente
- 3-5 projetos relacionados: Comece com Subtrees para simplicidade
- 5+ projetos ou equipes múltiplas: Nx é o investimento que compensa
Conclusão
Monorepos resolvem um problema real de coordenação, mas trazem complexidade. Git Subtrees oferece simplicidade para incorporação ocasional, perfeito quando você apenas quer incluir um código externo sem gerenciamento contínuo. Git Submodules é mais robusto para referências controladas, ideal quando múltiplos projetos compartilham a mesma biblioteca em versões específicas. Nx é a resposta quando você precisa escalar: não é apenas sobre organizar código no Git, mas sobre tornar builds, testes e deploys inteligentes e rápidos.
O erro mais comum é começar com Nx quando dois Submodules resolvem o problema, ou ficar com Subtrees quando Nx economizaria horas de CI/CD. A resposta correta é sempre: qual é o tamanho do meu time, quantos projetos tenho, e com que frequência código é compartilhado?
Referências
- Git Subtree Documentation — Documentação oficial do Git sobre Subtrees
- Git Submodules Guide — Documentação oficial do Git sobre Submodules
- Nx Documentation - Getting Started — Guia oficial de início com Nx
- Monorepo Workspaces - Nx Best Practices — Estruturas recomendadas para monorepos com Nx
- Effective Monorepo Architecture by Dan Shappir — Palestra sobre arquitetura de monorepos em larga escala