O que Todo Dev Deve Saber sobre Turborepo com TypeScript: Monorepo de Alta Performance Já leu

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: Agora crie a estrutura de diretórios: A raiz do monorepo contém

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:

  1. Analisa o grafo de dependências entre pacotes
  2. Determina ordem de compilação (utils → api → ui)
  3. Executa tarefas em paralelo sempre que possível
  4. 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.

Referências


Artigos relacionados