Voltar para o Blog

Monorepos com Nx e Turborepo: Como Escalar Projetos JavaScript em 2025

Olá HaWkers, monorepos tornaram-se o padrão de fato para projetos JavaScript de larga escala em 2025. Google, Facebook, Microsoft, Uber e praticamente todas as big techs gerenciam milhões de linhas de código em repositórios únicos.

Por quê? Porque monorepos com ferramentas modernas como Nx e Turborepo resolvem problemas que eram impossíveis de resolver com multi-repos: compartilhamento de código sem friction, builds incrementais que economizam horas, e refatoração atômica cross-projects.

Você ainda está gerenciando 15 repositórios separados que compartilham 80% do código? Está na hora de evoluir. Vamos explorar como monorepos modernos mudaram o jogo.

O Problema Que Monorepos Resolvem

Imagine: você tem um e-commerce com frontend web, mobile app, admin panel, APIs, e microserviços. No modelo tradicional (multi-repo):

Cenário Multi-Repo:

  • 8 repositórios diferentes
  • Cada um com seu package.json, CI/CD, versionamento
  • Código duplicado em todos (utils, types, constants)
  • Atualizar uma dependência? 8 PRs separados
  • Refatorar uma interface compartilhada? Coordenação de pesadelo
  • Builds independentes sem otimização

Cenário Monorepo (2025):

  • 1 repositório
  • Código compartilhado sem duplicação
  • Atualizar dependência? 1 commit
  • Refatoração atômica de toda a codebase
  • Builds inteligentes que só recompilam o que mudou
  • Cache distribuído compartilhado entre desenvolvedores
// Estrutura típica de Monorepo em 2025
monorepo-ecommerce/
├── apps/
│   ├── web/                  // Next.js app
│   ├── mobile/               // React Native
│   ├── admin/                // Admin panel
│   └── api/                  // Express API
├── packages/
│   ├── ui/                   // Shared components
│   ├── utils/                // Shared utilities
│   ├── types/                // Shared TypeScript types
│   ├── config/               // Shared configs
│   └── data-access/          // Shared API clients
├── tools/
│   ├── scripts/              // Build scripts
│   └── generators/           // Code generators
├── nx.json                   // Nx configuration
├── turbo.json                // Turborepo configuration
└── package.json              // Root package.json

// Antes (multi-repo): Código duplicado em 8 lugares
// packages/web/src/utils/formatCurrency.ts
// packages/mobile/src/utils/formatCurrency.ts
// packages/admin/src/utils/formatCurrency.ts
// ... (5 cópias mais)

// Depois (monorepo): Uma única fonte de verdade
// packages/utils/src/formatCurrency.ts
// Usado por TODOS os apps sem duplicação

Nx vs Turborepo: Qual Escolher?

Ambos dominam o mercado de 2025, mas têm filosofias diferentes:

Nx: O Framework Completo

# Criando workspace Nx
npx create-nx-workspace@latest my-monorepo

# Nx é opinativo e fornece tudo out-of-the-box

Vantagens do Nx:

  • Generators: Scaffolding automatizado de apps/libs
  • Dependency graph: Visualização clara de dependências
  • Affected commands: Roda apenas testes de código afetado
  • Plugin ecosystem: Integrações prontas (React, Next, Nest, etc)
  • Smart rebuilds: Cache inteligente local e remoto
// nx.json - Configuração Nx
{
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx-cloud",
      "options": {
        "cacheableOperations": ["build", "test", "lint"],
        "accessToken": "your-token",
        "parallel": 3
      }
    }
  },
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"]
    },
    "test": {
      "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"]
    }
  }
}

// Comandos poderosos
// Roda apenas testes afetados por mudanças
nx affected:test

// Builda apenas apps afetados
nx affected:build

// Visualiza dependency graph
nx graph

// Gera novo componente
nx generate @nx/react:component Button --project=ui

Turborepo: Minimalista e Rápido

# Criando workspace Turborepo
npx create-turbo@latest

Vantagens do Turborepo:

  • Simplicidade: Configuração minimal
  • Performance extrema: Builds paralelos agressivos
  • Remote caching: Vercel integração nativa
  • Zero config: Funciona com qualquer estrutura
  • Flexibilidade: Sem opinions fortes
// turbo.json - Configuração Turborepo
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"],
      "cache": true
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"],
      "cache": true
    },
    "lint": {
      "outputs": [],
      "cache": true
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  },
  "globalDependencies": [
    "tsconfig.json",
    ".eslintrc.js"
  ],
  "remoteCache": {
    "signature": true
  }
}
# Comandos Turborepo
# Build todos os packages
turbo build

# Build apenas o que mudou (incremental)
turbo build --filter=...HEAD

# Build com remote cache
turbo build --api="https://api.vercel.com" --token="your-token"

# Run em paralelo com limite
turbo build --concurrency=10

O Poder do Incremental Build

O diferencial dos monorepos modernos: nunca rebuild o que não mudou.

// Exemplo prático: E-commerce com 10 packages

// Cenário 1: SEM incremental build
// Desenvolvedor muda 1 linha em packages/utils
// Build tradicional:
// ❌ Rebuilds: web (5min) + mobile (8min) + admin (4min) + api (3min)
// ❌ Total: 20 minutos

// Cenário 2: COM Nx/Turborepo
// Mesmo change em packages/utils
// Build inteligente:
// ✅ Rebuilds apenas: utils (10s) + apps que dependem dele
// ✅ Usa cache para resto
// ✅ Total: 2 minutos

// Economia: 18 minutos = 90% mais rápido!

Implementação Prática: Remote Caching

// packages/build-tools/cache-utils.ts
import { createHash } from 'crypto';
import { readFileSync } from 'fs';

export class BuildCache {
  private cacheEndpoint: string;

  constructor(endpoint: string) {
    this.cacheEndpoint = endpoint;
  }

  async getCachedBuild(inputs: string[]): Promise<Buffer | null> {
    // Gera hash dos inputs
    const hash = this.hashInputs(inputs);

    try {
      // Busca no cache remoto
      const response = await fetch(`${this.cacheEndpoint}/cache/${hash}`);

      if (response.ok) {
        console.log(`✅ Cache HIT for ${hash}`);
        return Buffer.from(await response.arrayBuffer());
      }

      console.log(`❌ Cache MISS for ${hash}`);
      return null;
    } catch (error) {
      console.warn('Cache fetch failed:', error);
      return null;
    }
  }

  async storeBuild(inputs: string[], output: Buffer): Promise<void> {
    const hash = this.hashInputs(inputs);

    try {
      await fetch(`${this.cacheEndpoint}/cache/${hash}`, {
        method: 'PUT',
        body: output,
        headers: {
          'Content-Type': 'application/octet-stream'
        }
      });

      console.log(`✅ Stored build in cache: ${hash}`);
    } catch (error) {
      console.warn('Cache store failed:', error);
    }
  }

  private hashInputs(inputs: string[]): string {
    const hash = createHash('sha256');

    for (const input of inputs) {
      const content = readFileSync(input);
      hash.update(content);
    }

    return hash.digest('hex');
  }

  async executeWithCache<T>(
    inputs: string[],
    buildFn: () => Promise<T>
  ): Promise<T> {
    // Tenta buscar do cache
    const cached = await this.getCachedBuild(inputs);

    if (cached) {
      return JSON.parse(cached.toString()) as T;
    }

    // Se não encontrou, executa build
    const result = await buildFn();

    // Armazena no cache
    await this.storeBuild(inputs, Buffer.from(JSON.stringify(result)));

    return result;
  }
}

// Uso em build script
const cache = new BuildCache('https://cache.mycompany.com');

async function buildPackage(packageName: string) {
  const inputs = [
    `packages/${packageName}/src/**/*.ts`,
    `packages/${packageName}/package.json`,
    `packages/${packageName}/tsconfig.json`
  ];

  return cache.executeWithCache(inputs, async () => {
    console.log(`🔨 Building ${packageName}...`);

    // Build real aqui
    const result = await runBuild(packageName);

    return result;
  });
}

Dependency Graph: Entendendo Impactos

// packages/dependency-analyzer/graph.ts
import { ProjectGraph } from '@nx/devkit';

class DependencyAnalyzer {
  constructor(private graph: ProjectGraph) {}

  findAffectedProjects(changedFiles: string[]): Set<string> {
    const affected = new Set<string>();

    // Para cada arquivo mudado
    for (const file of changedFiles) {
      const project = this.findProjectByFile(file);

      if (project) {
        // Adiciona projeto diretamente afetado
        affected.add(project);

        // Adiciona todos os projetos que dependem dele
        const dependents = this.findDependents(project);
        dependents.forEach(dep => affected.add(dep));
      }
    }

    return affected;
  }

  findDependents(projectName: string): Set<string> {
    const dependents = new Set<string>();

    // Busca todos os projetos que dependem de projectName
    for (const [name, project] of Object.entries(this.graph.nodes)) {
      const deps = project.data.dependencies || [];

      if (deps.some(dep => dep.target === projectName)) {
        dependents.add(name);

        // Recursivo: encontra dependents dos dependents
        const transitive = this.findDependents(name);
        transitive.forEach(dep => dependents.add(dep));
      }
    }

    return dependents;
  }

  estimateBuildTime(projects: Set<string>): number {
    let totalTime = 0;

    // Calcula tempo estimado baseado em builds anteriores
    for (const project of projects) {
      const avgTime = this.getAverageBuildTime(project);
      totalTime += avgTime;
    }

    return totalTime;
  }

  private getAverageBuildTime(project: string): number {
    // Em produção, busca de analytics/histórico
    const buildTimes: Record<string, number> = {
      'web': 300,      // 5 min
      'mobile': 480,   // 8 min
      'admin': 240,    // 4 min
      'api': 180,      // 3 min
      'ui': 60,        // 1 min
      'utils': 30      // 30 sec
    };

    return buildTimes[project] || 120;
  }

  generateBuildPlan(changedFiles: string[]): {
    projects: string[];
    estimatedTime: number;
    canParallelize: string[][];
  } {
    const affected = this.findAffectedProjects(changedFiles);

    // Determina quais podem rodar em paralelo
    const parallelGroups = this.groupParallelBuilds(Array.from(affected));

    return {
      projects: Array.from(affected),
      estimatedTime: this.estimateBuildTime(affected),
      canParallelize: parallelGroups
    };
  }

  private groupParallelBuilds(projects: string[]): string[][] {
    // Agrupa projetos que podem ser buildados em paralelo
    // Projetos no mesmo grupo não têm dependências entre si
    const groups: string[][] = [];
    const processed = new Set<string>();

    for (const project of projects) {
      if (processed.has(project)) continue;

      const group = [project];
      processed.add(project);

      // Encontra projetos que podem rodar junto
      for (const other of projects) {
        if (processed.has(other)) continue;

        const hasSharedDeps = this.haveSharedDependencies(project, other);

        if (!hasSharedDeps) {
          group.push(other);
          processed.add(other);
        }
      }

      groups.push(group);
    }

    return groups;
  }

  private haveSharedDependencies(p1: string, p2: string): boolean {
    const deps1 = this.graph.nodes[p1]?.data.dependencies || [];
    const deps2 = this.graph.nodes[p2]?.data.dependencies || [];

    return deps1.some(d1 =>
      deps2.some(d2 => d1.target === d2.target)
    );
  }

  private findProjectByFile(file: string): string | null {
    for (const [name, project] of Object.entries(this.graph.nodes)) {
      const root = project.data.root;

      if (file.startsWith(root)) {
        return name;
      }
    }

    return null;
  }
}

// Uso em CI/CD
const analyzer = new DependencyAnalyzer(projectGraph);

const changedFiles = getChangedFilesSinceLast Commit();
const buildPlan = analyzer.generateBuildPlan(changedFiles);

console.log('📊 Build Plan:');
console.log(`  Affected projects: ${buildPlan.projects.length}`);
console.log(`  Estimated time: ${Math.ceil(buildPlan.estimatedTime / 60)} minutes`);
console.log(`  Parallel groups: ${buildPlan.canParallelize.length}`);

// Executa builds em paralelo
for (const group of buildPlan.canParallelize) {
  await Promise.all(
    group.map(project => buildPackage(project))
  );
}

Quando Usar Monorepos?

Monorepos não são para todos os casos. Use quando:

Múltiplos projetos relacionados: Frontend + Backend + Mobile sharing code
Frequente sharing de código: Components, utils, types
Refatoração cross-project: Mudanças que afetam múltiplos projetos
CI/CD complexo: Precisa orquestrar builds de múltiplos projetos

Evite quando:

  • Projeto único simples
  • Times completamente independentes
  • Projetos sem código compartilhado
  • Infraestrutura de CI limitada

Conclusão: O Futuro do Gerenciamento de Código

Monorepos com Nx ou Turborepo tornaram-se ferramenta essencial para projetos modernos. A capacidade de escalar sem friction, combinar com builds inteligentes e cache distribuído, e ter refatoração atômica fazem deles escolha óbvia para 2025.

Se você está interessado em outras tendências de arquitetura moderna, veja TypeScript em 2025: Dominando o Mercado, onde exploramos como Type Safety se integra perfeitamente com monorepos.

Bora pra cima! 🦅

📚 Quer Aprofundar Seus Conhecimentos em JavaScript?

Este artigo cobriu monorepos e arquitetura avançada, mas há muito mais para explorar no mundo do desenvolvimento moderno.

Desenvolvedores que investem em conhecimento sólido e estruturado tendem a ter mais oportunidades no mercado.

Material de Estudo Completo

Se você quer dominar JavaScript do básico ao avançado, preparei um guia completo:

Opções de investimento:

  • R$9,90 (pagamento único)

👉 Conhecer o Guia JavaScript

💡 Material atualizado com as melhores práticas do mercado

Comentários (0)

Esse artigo ainda não possui comentários 😢. Seja o primeiro! 🚀🦅

Adicionar comentário