Voltar para Artigos
Front-end12 min de leitura

A Ascensão dos Monorepos: Arquitetura com Turborepo

Do problema concreto de duplicação de código entre repositórios ao setup completo de um monorepo com PNPM Workspaces, Turborepo, Remote Caching e pacotes compartilhados.

12 de agosto de 2026

Você tem um painel Admin em React, um e-commerce em Next.js e uma API em Node.js. Cada um em seu próprio repositório. Parece organizado — até o dia em que você precisa atualizar o componente <Button> do design system. São três PRs, três pipelines de CI, três revisões e inevitavelmente um dos repos fica desatualizado por semanas. Esse é o problema concreto que Monorepos resolvem.

A arquitetura de Monorepo coloca todos os projetos em um único repositório Git, divididos em workspaces. O Turborepo é o orquestrador de builds que torna isso viável em escala — com cache inteligente, paralelismo e Remote Cache compartilhado entre máquinas.

Monorepo vs Polyrepo: O Tradeoff Real

A decisão entre monorepo e polyrepo não é técnica — é organizacional. Antes de avançar, entenda o contexto em que cada um faz sentido:

  • Polyrepo faz sentido quando os times são completamente independentes, os produtos têm ciclos de release distintos, ou você precisa de permissões de acesso granulares por repositório.
  • Monorepo faz sentido quando múltiplos produtos compartilham componentes, tipos TypeScript, configurações ou regras de negócio. Quando um bug em um pacote compartilhado precisa ser corrigido e testado em todos os consumidores ao mesmo tempo.
  • Empresas que usam monorepo em produção: Google (todo código em um repo), Meta, Airbnb, Vercel (Next.js + Turbo no mesmo repo), Nx (próprio monorepo de ferramentas).

Estrutura de um Monorepo com PNPM Workspaces

O PNPM é o gerenciador de pacotes preferido para monorepos por causa do seu node_modules baseado em links simbólicos (hardlinks + symlinks), que ocupa muito menos espaço em disco do que npm ou yarn ao lidar com múltiplos workspaces. Aqui está a estrutura ideal:

text
meu-monorepo/
├── apps/
│   ├── web/              # Next.js — loja/site público
│   ├── admin/            # React + Vite — painel administrativo
│   └── api/              # Node.js + Express — backend
├── packages/
│   ├── ui/               # Componentes React compartilhados
│   ├── types/            # Interfaces TypeScript compartilhadas
│   ├── utils/            # Funções utilitárias (formatação, validação)
│   ├── eslint-config/    # Config do ESLint da empresa
│   └── tsconfig/         # Configs base do TypeScript
├── turbo.json            # Configuração do Turborepo
├── pnpm-workspace.yaml   # Define os workspaces do PNPM
└── package.json          # Root — scripts e devDependencies globais
pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'

Criando Pacotes Compartilhados

O ponto central de um monorepo é que apps/web importa de packages/ui como se fosse uma dependência npm normal. Veja como configurar o pacote @acme/ui:

package.json
{
  "name": "@acme/ui",
  "version": "0.0.1",
  "private": true,
  "exports": {
    "./*": {
      "import": "./src/*.tsx",
      "types": "./src/*.tsx"
    }
  },
  "peerDependencies": {
    "react": "^18",
    "react-dom": "^18"
  },
  "devDependencies": {
    "@acme/tsconfig": "workspace:*",
    "typescript": "catalog:"
  }
}

Agora no apps/web/package.json, instale o pacote local com a sintaxe workspace:*:

package.json
{
  "name": "web",
  "dependencies": {
    "@acme/ui": "workspace:*",
    "@acme/types": "workspace:*",
    "next": "15.0.0",
    "react": "^18"
  }
}

E no código do Next.js, simplesmente importe:

page.tsx
// Importa o Button do pacote compartilhado — sem duplicação
import { Button } from '@acme/ui/button';
import type { Product } from '@acme/types/product';

export default function HomePage() {
  return (
    <main>
      <Button variant="primary">Ver produtos</Button>
    </main>
  );
}

Use o campo "catalog" do PNPM (disponível desde v9) para definir versões de dependências uma única vez na raiz e reutilizar em todos os workspaces. Isso elimina a dessincronização de versões — o pior inimigo de monorepos grandes.

Configurando o Turborepo

O Turborepo lê o turbo.json na raiz para entender como os workspaces se relacionam e como cachear cada tarefa:

turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "ui": "tui",
  "tasks": {
    "build": {
      // Este workspace só pode buildar depois que suas dependências buildarem
      "dependsOn": ["^build"],
      // Quais arquivos fazem parte da saída (para cache)
      "outputs": [".next/**", "!.next/cache/**", "dist/**"]
    },
    "dev": {
      "cache": false,      // Dev server não deve ser cacheado
      "persistent": true   // Processo de longa duração (watch mode)
    },
    "lint": {
      // Lint pode rodar em paralelo, sem dependências entre workspaces
      "dependsOn": []
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"]
    },
    "typecheck": {
      "dependsOn": ["^build"]
    }
  }
}

O operador ^ em "^build" é a chave do Turborepo: significa "as dependências do grafo devem executar build antes de mim". Então se apps/web depende de @acme/ui, o Turborepo garante que packages/ui seja buildado primeiro, e só então inicia o build do apps/web.

Como o Cache do Turborepo Funciona

O Turborepo cria um hash de cada tarefa baseado em: arquivos de entrada, variáveis de ambiente relevantes, e os outputs das dependências. Se nada mudou, ele restaura o output cacheado sem executar o comando. Isso significa que um build que levava 4 minutos pode levar 200ms no segundo run.

bash
# Primeiro run: executa tudo
pnpm turbo run build

# Saída típica:
# packages/tsconfig:build: cache miss, executing...
# packages/ui:build: cache miss, executing...
# apps/web:build: cache miss, executing...
# Total: 4m 12s

# Segundo run sem alterações:
pnpm turbo run build

# Saída típica:
# packages/tsconfig:build: cache hit, replaying output...
# packages/ui:build: cache hit, replaying output...
# apps/web:build: cache hit, replaying output...
# Total: 187ms  ← 99% mais rápido

Remote Caching: Compartilhando Cache entre Máquinas

O Remote Cache é onde o Turborepo realmente brilha em equipes. Por padrão, o cache fica local (./node_modules/.cache/turbo). Com o Remote Cache habilitado, o hash de cada tarefa é sincronizado com a Vercel — então quando o CI roda um build que você já executou na sua máquina, ele simplesmente baixa o cache em vez de recomputar.

bash
# Autenticando com a Vercel para Remote Cache
npx turbo login
npx turbo link

# A partir daí, turbo run build automaticamente usa e popula o Remote Cache.
# Você pode usar servidores self-hosted também:
# npx turbo run build --remote-cache-read-only  (CI read-only)
# --api="https://seu-cache-server.com" --token="TOKEN"

Alternativas self-hosted ao Remote Cache da Vercel incluem o Turborepo Remote Cache open-source (ducktape/turborepo-remote-cache no GitHub) e o Nx Cloud. Para projetos Open Source ou times pequenos, o tier gratuito da Vercel é mais do que suficiente.

Scripts Globais e Filtragem por Workspace

Um dos maiores ganhos de DX em monorepos é poder rodar comandos em subconjuntos específicos de workspaces:

bash
# Roda o dev server apenas do apps/web
pnpm turbo run dev --filter=web

# Roda build de todos os apps (mas não packages)
pnpm turbo run build --filter='./apps/*'

# Roda lint apenas dos workspaces que sofreram alterações
# em relação à branch main (útil em PRs)
pnpm turbo run lint --filter='...[origin/main]'

# Roda build do @acme/ui e todos os seus consumidores
pnpm turbo run build --filter='...@acme/ui'

# No package.json da raiz, scripts convenientes:
# "dev": "turbo run dev",
# "build": "turbo run build",
# "lint": "turbo run lint",
# "test": "turbo run test"

Integrando com GitHub Actions

Com Remote Cache habilitado, o CI fica dramaticamente mais rápido. Um workflow otimizado para monorepos:

ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        with:
          # Fetch completo para o --filter funcionar com histórico de commits
          fetch-depth: 2

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Lint, Test, Build
        run: pnpm turbo run lint test build
        env:
          # Token do Remote Cache Vercel
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}
          # Turborepo usa isso para identificar o pipeline no Remote Cache
          TURBO_REMOTE_ONLY: true

Configuração Compartilhada de TypeScript e ESLint

Um dos maiores benefícios concretos: um único arquivo tsconfig base e uma única config do ESLint para toda a empresa.

base.json
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022"],
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "exactOptionalPropertyTypes": true,
    "noUncheckedIndexedAccess": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "resolveJsonModule": true
  }
}
tsconfig.json
{
  "extends": "@acme/tsconfig/nextjs.json",
  "compilerOptions": {
    "plugins": [{ "name": "next" }],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

Conclusão

Monorepos com Turborepo não são complexidade gratuita — são uma resposta a problemas reais de escala. A combinação de PNPM Workspaces (espaço em disco eficiente, symlinks), Turborepo (cache de builds, paralelismo inteligente) e Remote Cache (compartilhamento entre máquinas) transforma um pipeline de CI de 10 minutos em algo sub-minuto na maioria dos PRs.

O ponto de virada para adotar um monorepo é quando você se pega abrindo mais de um repositório para implementar uma única feature. Nesse momento, o overhead de sincronização entre repos supera o overhead de configurar um monorepo. Para times pequenos ainda crescendo, vale começar com um monorepo desde o início — é muito mais fácil separar depois do que unir.