Entendendo Monorepos e o Papel do Turborepo
Um monorepo é um repositório único que contém múltiplos projetos independentes, seja aplicações web, bibliotecas internas, ferramentas CLI ou pacotes npm. Diferente da abordagem tradicional de polyrepo (um repositório por projeto), o monorepo centraliza o controle de versão, facilitando o compartilhamento de código, manutenção de dependências e sincronização entre projetos relacionados.
O Turborepo é um orquestrador de compilação e tarefas construído especificamente para otimizar o desempenho em monorepos. Ele resolve dois problemas críticos: evita reprocessamento desnecessário através de cache inteligente e executa tarefas em paralelo respeitando dependências entre pacotes. Quando você trabalha com dezenas de pacotes, essas otimizações fazem diferença mensurável — builds que levavam minutos caem para segundos.
Configuração Inicial: Estrutura e Setup
Criando a Estrutura Base
Comece criando um novo workspace monorepo com TypeScript. A estrutura padrão segue uma organização clara onde cada pacote é autossuficiente mas compartilha configurações globais:
mkdir meu-monorepo && cd meu-monorepo
npm init -y
npm install -D turbo typescript @types/node
Agora crie a estrutura de diretórios:
meu-monorepo/
├── packages/
│ ├── ui/
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ └── src/
│ ├── utils/
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ └── src/
│ └── api/
│ ├── package.json
│ ├── tsconfig.json
│ └── src/
├── turbo.json
├── tsconfig.json
├── package.json
└── pnpm-workspace.yaml (ou yarn.lock)
A raiz do monorepo contém a configuração compartilhada, enquanto cada pacote dentro de packages/ é um projeto independente com seu próprio package.json e tsconfig.json.
Configurando o turbo.json
O arquivo turbo.json define como o Turborepo executa suas tarefas. Este é o coração da orquestração:
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["tsconfig.json", ".env"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"],
"cache": true
},
"lint": {
"outputs": [],
"cache": true
},
"test": {
"outputs": ["coverage/**"],
"cache": false
},
"dev": {
"cache": false,
"persistent": true
}
}
}
A chave dependsOn: ["^build"] significa que uma tarefa build em um pacote só executa após o build completar em seus pacotes dependentes (indicado pelo ^). Isso garante ordem correta de compilação. As outputs indicam quais arquivos gerados devem ser cacheados — o Turborepo armazena esses resultados e reutiliza em execuções subsequentes quando nada mudou.
Configuração de TypeScript Compartilhada
Na raiz, crie um tsconfig.json base:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020"],
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
}
}
Em packages/utils/tsconfig.json, estenda a configuração raiz:
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Estruturando Pacotes Interdependentes com TypeScript
Criando um Pacote de Utilitários
Começamos com um pacote simples que será compartilhado entre outros. Em packages/utils/package.json:
{
"name": "@monorepo/utils",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"lint": "eslint src/**/*.ts"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}
Crie packages/utils/src/validators.ts:
export interface ValidationResult {
valid: boolean;
errors: string[];
}
export function validateEmail(email: string): ValidationResult {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const valid = regex.test(email);
return {
valid,
errors: valid ? [] : ['Email format is invalid']
};
}
export function validatePassword(password: string): ValidationResult {
const errors: string[] = [];
if (password.length < 8) {
errors.push('Password must be at least 8 characters');
}
if (!/[A-Z]/.test(password)) {
errors.push('Password must contain uppercase letter');
}
if (!/[0-9]/.test(password)) {
errors.push('Password must contain a number');
}
return {
valid: errors.length === 0,
errors
};
}
E o arquivo de entrada packages/utils/src/index.ts:
export * from './validators';
export { ValidationResult } from './validators';
Criando um Pacote que Depende de Outro
Agora crie um pacote api que utilizará o pacote utils. Em packages/api/package.json:
{
"name": "@monorepo/api",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "node -r ts-node/register src/server.ts",
"lint": "eslint src/**/*.ts"
},
"dependencies": {
"@monorepo/utils": "*"
},
"devDependencies": {
"typescript": "^5.0.0",
"ts-node": "^10.0.0"
}
}
Crie packages/api/src/user-service.ts:
import { validateEmail, validatePassword, ValidationResult } from '@monorepo/utils';
export interface User {
id: string;
email: string;
name: string;
}
export interface RegisterRequest {
email: string;
password: string;
name: string;
}
export class UserService {
private users: Map<string, User> = new Map();
private nextId: number = 1;
register(request: RegisterRequest): { success: boolean; user?: User; errors?: string[] } {
// Validar email
const emailValidation = validateEmail(request.email);
if (!emailValidation.valid) {
return { success: false, errors: emailValidation.errors };
}
// Validar password
const passwordValidation = validatePassword(request.password);
if (!passwordValidation.valid) {
return { success: false, errors: passwordValidation.errors };
}
// Criar usuário
const user: User = {
id: String(this.nextId++),
email: request.email,
name: request.name
};
this.users.set(user.id, user);
return { success: true, user };
}
getUser(id: string): User | undefined {
return this.users.get(id);
}
}
Observe que o import é feito diretamente de @monorepo/utils graças à configuração do workspace npm/pnpm. O Turborepo garante que quando você compilar este pacote, o pacote utils já terá sido compilado.
Otimizações, Caching e Execução em Paralelo
Compreendendo o Sistema de Cache
O Turborepo calcula um hash para cada tarefa baseado em: arquivos de entrada, scripts, variáveis de ambiente e outputs anteriores. Se nada mudou, reutiliza o cache. Isso é transformador em CI/CD pipelines.
Configure em turbo.json para ser mais agressivo com cache:
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", "build/**"],
"cache": true,
"hashAlgorithm": "sha256"
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"],
"cache": true
}
},
"caching": {
"outputLogs": "errors-only"
}
}
Execute no seu monorepo:
npx turbo run build
# Na segunda execução (sem mudanças):
npx turbo run build
# Verá: ">>> FULL TURBO [cached]" — extremamente rápido
Executando Tarefas em Paralelo com Dependências
Adicione um script no package.json raiz para execução completa:
{
"scripts": {
"build": "turbo run build",
"build:ui": "turbo run build --filter=ui",
"build:api": "turbo run build --filter=api --filter=utils",
"dev": "turbo run dev --parallel",
"lint": "turbo run lint --parallel",
"test": "turbo run test --parallel"
}
}
O parâmetro --parallel executa tarefas que não têm dependências entre si simultaneamente. --filter permite limitar execução a pacotes específicos. Quando você roda turbo run build, o sistema:
- Analisa o grafo de dependências entre pacotes
- Determina ordem de compilação (utils → api → ui)
- Executa tarefas em paralelo sempre que possível
- Reutiliza cache se disponível
Filtrando Execução para Desenvolvimento
Para desenvolvimento local, frequentemente você quer rodar apenas pacotes modificados:
# Execute dev apenas em pacotes que mudaram
turbo run dev --filter='...[origin/main]'
# Execute em um pacote e suas dependências
turbo run build --filter=@monorepo/api
A sintaxe ...[origin/main] (com GitDiff) roda tarefas apenas em pacotes que alteraram desde a branch main, economizando tempo em monorepos grandes.
Conclusão
Você aprendeu que o Turborepo transforma o desenvolvimento em monorepos através de três mecanismos essenciais: orquestração clara de dependências (definida em turbo.json), sistema de cache inteligente que reutiliza outputs quando inputs não mudam, e execução paralela respeitosa de dependências que compila apenas o necessário na ordem correta. Essas otimizações são críticas em equipes onde múltiplos pacotes compartilham código e precisam ser compilados juntos com frequência.