Turborepo: como organizar um monorepo Node.js que realmente funciona
1. Por que escolher Turborepo para seu monorepo Node.js?
Manter múltiplos projetos Node.js em um único repositório sem ferramentas especializadas rapidamente se torna um pesadelo. Dependências duplicadas, builds lentos e scripts espalhados por dezenas de package.json são problemas comuns que afetam a produtividade da equipe. O Turborepo surge como uma solução elegante para esses desafios.
Comparado a alternativas como Nx, Lerna ou Yarn Workspaces puro, o Turborepo se destaca por três características principais:
- Cache inteligente: cada tarefa executada gera um hash baseado nos inputs (código-fonte, dependências, configurações). Se nada mudou, o resultado é restaurado do cache em milissegundos.
- Paralelismo automático: tarefas independentes são executadas simultaneamente, aproveitando ao máximo os recursos da máquina.
- Simplicidade de configuração: com poucas linhas no
turbo.json, você define pipelines completos sem complexidade desnecessária.
Enquanto o Nx oferece mais funcionalidades (geração de código, plugins avançados), o Turborepo foca no essencial: build rápido e confiável para monorepos Node.js. Para a maioria dos projetos, essa simplicidade é um diferencial competitivo.
2. Configuração inicial do Turborepo
A estrutura de pastas recomendada segue o padrão:
meu-monorepo/
├── apps/
│ ├── web/ # Aplicação Next.js
│ ├── api/ # Backend Express
│ └── mobile/ # App React Native
├── packages/
│ ├── ui/ # Componentes compartilhados
│ ├── utils/ # Funções utilitárias
│ └── config/ # Configurações ESLint, TypeScript
├── tooling/
│ └── eslint/ # Pacote de regras personalizadas
├── turbo.json
├── package.json
└── pnpm-workspace.yaml
O arquivo turbo.json é o coração da configuração:
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**", "!.next/cache/**"]
},
"test": {
"dependsOn": ["build"],
"inputs": ["src/**/*.ts", "src/**/*.tsx", "test/**/*.ts"]
},
"lint": {
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
}
}
}
A integração com workspaces é feita no package.json raiz:
{
"private": true,
"workspaces": ["apps/*", "packages/*", "tooling/*"],
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"test": "turbo run test"
},
"devDependencies": {
"turbo": "^1.10.0"
}
}
3. Pipeline de build: otimizando o fluxo de trabalho
Definir tarefas corretamente no pipeline é crucial. Cada tarefa pode ter dependências e outputs específicos:
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"],
"inputs": ["src/**", "tsconfig.json"]
},
"typecheck": {
"dependsOn": ["^build"],
"outputs": []
},
"deploy": {
"dependsOn": ["build", "test"],
"outputs": [],
"cache": false
}
}
}
O cache remoto é um dos maiores diferenciais. Com a Vercel, a configuração é simples:
# .env
TURBO_TOKEN=seu_token_vercel
TURBO_TEAM=sua_equipe
TURBO_REMOTE_CACHE_SIGNATURE_KEY=chave_opcional
Em CI, isso reduz o tempo de build de 15 minutos para 2 minutos quando apenas pacotes específicos são alterados.
4. Gerenciamento de dependências entre pacotes
O Turborepo usa o sistema de workspaces do gerenciador de pacotes (npm, yarn, pnpm) para resolver dependências internas. A configuração de dependências entre pacotes segue o padrão:
// packages/ui/package.json
{
"name": "@meuapp/ui",
"dependencies": {
"@meuapp/utils": "workspace:*",
"react": "^18.0.0"
}
}
O dependsOn no turbo.json controla a ordem de execução:
{
"pipeline": {
"build": {
"dependsOn": ["^build"]
}
}
}
O símbolo ^ indica que a tarefa build de um pacote depende da tarefa build de todos os seus pacotes dependentes. Sem ele, as tarefas seriam executadas em paralelo, causando erros.
Para evitar o "dependency hell", mantenha um único lockfile (gerado pelo pnpm ou yarn) e use versionamento semântico rigoroso nos pacotes internos.
5. Trabalhando em equipe: boas práticas de colaboração
A padronização de scripts é essencial em equipe:
// package.json raiz
{
"scripts": {
"dev:web": "turbo run dev --filter=@meuapp/web",
"dev:api": "turbo run dev --filter=@meuapp/api",
"test:changed": "turbo run test --filter=[HEAD^1]",
"lint:all": "turbo run lint"
}
}
Para hooks de pré-commit com Husky e lint-staged:
// .husky/pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx turbo run lint test --filter=[HEAD^1]
// lint-staged.config.js
module.exports = {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"]
}
Essa configuração garante que apenas os pacotes alterados sejam verificados, mantendo o fluxo rápido mesmo em monorepos grandes.
6. Deploy e CI/CD com Turborepo
Um pipeline de CI eficiente usa o cache remoto e filtros inteligentes:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- run: pnpm install
- name: Turbo Cache
uses: actions/cache@v3
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
- run: pnpm turbo run build test --filter=[HEAD^1]
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
Para deploy seletivo, use scripts que verificam quais pacotes foram alterados:
#!/bin/bash
# scripts/deploy-changed.sh
CHANGED_PACKAGES=$(pnpm turbo run build --filter=[HEAD^1] --dry-run=json | jq -r '.packages[]')
for PACKAGE in $CHANGED_PACKAGES; do
if [[ $PACKAGE == @meuapp/web ]]; then
vercel deploy --prod
elif [[ $PACKAGE == @meuapp/api ]]; then
railway up
fi
done
7. Armadilhas comuns e como evitá-las
Problema com hoisting: dependências conflitantes entre pacotes podem causar erros misteriosos.
Solução: use pnpm com shamefully-hoist=false no .npmrc:
# .npmrc
shamefully-hoist=false
strict-peer-dependencies=false
Cache quebrado por inputs incorretos: se você não especificar corretamente os inputs, o Turborepo pode não detectar mudanças.
// Errado: inputs muito amplos
"build": {
"inputs": ["**"]
}
// Correto: específico
"build": {
"inputs": ["src/**", "tsconfig.json", "package.json"]
}
Outputs mal configurados: pastas de cache do Next.js (.next/cache) devem ser excluídas:
"outputs": [".next/**", "!.next/cache/**"]
Quando Turborepo não é a melhor escolha: para projetos com menos de 3 pacotes ou equipes muito pequenas, a complexidade adicional pode não valer a pena. Nesses casos, Yarn Workspaces puro ou até mesmo repositórios separados são mais adequados.
O Turborepo brilha quando você precisa escalar um monorepo Node.js com dezenas de pacotes e múltiplos times trabalhando simultaneamente. Com cache inteligente, paralelismo e configuração simples, ele transforma o caos de dependências em um fluxo de trabalho previsível e rápido.
Referências
- Documentação oficial do Turborepo — Guia completo de configuração, pipelines e cache remoto
- Turborepo: Getting Started Guide — Tutorial passo a passo para criar seu primeiro monorepo
- Vercel Turborepo Remote Caching — Como configurar cache remoto com Vercel para acelerar builds em CI
- Monorepos com Turborepo e pnpm — Artigo técnico detalhando integração entre Turborepo e pnpm
- Turborepo vs Nx: Qual escolher? — Comparação prática entre as principais ferramentas de monorepo
- Husky + lint-staged em monorepos — Guia oficial para configurar hooks de git em projetos com workspaces
- GitHub Actions + Turborepo CI Pipeline — Exemplo oficial de pipeline de CI/CD com Turborepo e GitHub Actions