Volver al blog

Monorepos con Nx y Turborepo: Cómo Escalar Proyectos JavaScript en 2025

Hola HaWkers, monorepos se convirtieron en el estándar de facto para proyectos JavaScript de larga escala en 2025. Google, Facebook, Microsoft, Uber y prácticamente todas las big techs gerencian millones de líneas de código en repositorios únicos.

¿Por qué? Porque monorepos con herramientas modernas como Nx y Turborepo resuelven problemas que eran imposibles de resolver con multi-repos: compartir código sin friction, builds incrementales que economizan horas, y refactorización atómica cross-projects.

¿Todavía estás gerenciando 15 repositorios separados que comparten 80% del código? Es hora de evolucionar. Vamos a explorar cómo monorepos modernos cambiaron el juego.

El Problema Que Monorepos Resuelven

Imagina: tienes un e-commerce con frontend web, mobile app, admin panel, APIs, y microservicios. En el modelo tradicional (multi-repo):

Escenario Multi-Repo:

  • 8 repositorios diferentes
  • Cada uno con su package.json, CI/CD, versionamiento
  • Código duplicado en todos (utils, types, constants)
  • ¿Actualizar una dependencia? 8 PRs separados
  • ¿Refactorizar una interface compartida? Coordinación de pesadilla
  • Builds independientes sin optimización

Escenario Monorepo (2025):

  • 1 repositorio
  • Código compartido sin duplicación
  • ¿Actualizar dependencia? 1 commit
  • Refactorización atómica de toda la codebase
  • Builds inteligentes que solo recompilan lo que cambió
  • Cache distribuido compartido entre desarrolladores
// Estructura típica de Monorepo en 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 en 8 lugares
// packages/web/src/utils/formatCurrency.ts
// packages/mobile/src/utils/formatCurrency.ts
// packages/admin/src/utils/formatCurrency.ts
// ... (5 copias más)

// Después (monorepo): Una única fuente de verdad
// packages/utils/src/formatCurrency.ts
// Usado por TODOS los apps sin duplicación

Nx vs Turborepo: ¿Cuál Elegir?

Ambos dominan el mercado de 2025, pero tienen filosofías diferentes:

Nx: El Framework Completo

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

# Nx es opinativo y provee todo out-of-the-box

Ventajas de Nx:

  • Generators: Scaffolding automatizado de apps/libs
  • Dependency graph: Visualización clara de dependencias
  • Affected commands: Corre apenas tests de código afectado
  • Plugin ecosystem: Integraciones listas (React, Next, Nest, etc)
  • Smart rebuilds: Cache inteligente local y remoto
// nx.json - Configuración 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
// Corre apenas tests afectados por cambios
nx affected:test

// Builda apenas apps afectados
nx affected:build

// Visualiza dependency graph
nx graph

// Genera nuevo componente
nx generate @nx/react:component Button --project=ui

Turborepo: Minimalista y Rápido

# Creando workspace Turborepo
npx create-turbo@latest

Ventajas de Turborepo:

  • Simplicidad: Configuración minimal
  • Performance extrema: Builds paralelos agresivos
  • Remote caching: Vercel integración nativa
  • Zero config: Funciona con cualquier estructura
  • Flexibilidad: Sin opinions fuertes
// turbo.json - Configuración 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 los packages
turbo build

# Build apenas lo que cambió (incremental)
turbo build --filter=...HEAD

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

# Run en paralelo con límite
turbo build --concurrency=10

El Poder del Incremental Build

El diferencial de los monorepos modernos: nunca rebuild lo que no cambió.

// Ejemplo práctico: E-commerce con 10 packages

// Escenario 1: SIN incremental build
// Desarrollador cambia 1 línea en packages/utils
// Build tradicional:
// ❌ Rebuilds: web (5min) + mobile (8min) + admin (4min) + api (3min)
// ❌ Total: 20 minutos

// Escenario 2: CON Nx/Turborepo
// Mismo cambio en packages/utils
// Build inteligente:
// ✅ Rebuilds apenas: utils (10s) + apps que dependen de él
// ✅ Usa cache para resto
// ✅ Total: 2 minutos

// Economía: 18 minutos = 90% más rápido!

Implementación Práctica: 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> {
    // Genera hash de los inputs
    const hash = this.hashInputs(inputs);

    try {
      // Busca en 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> {
    // Intenta buscar del cache
    const cached = await this.getCachedBuild(inputs);

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

    // Si no encontró, ejecuta build
    const result = await buildFn();

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

    return result;
  }
}

// Uso en 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 aquí
    const result = await runBuild(packageName);

    return result;
  });
}

Dependency Graph: Entendiendo 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 archivo cambiado
    for (const file of changedFiles) {
      const project = this.findProjectByFile(file);

      if (project) {
        // Agrega proyecto directamente afectado
        affected.add(project);

        // Agrega todos los proyectos que dependen de él
        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 los proyectos que dependen 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: encuentra dependents de los dependents
        const transitive = this.findDependents(name);
        transitive.forEach(dep => dependents.add(dep));
      }
    }

    return dependents;
  }

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

    // Calcula tiempo estimado basado en builds anteriores
    for (const project of projects) {
      const avgTime = this.getAverageBuildTime(project);
      totalTime += avgTime;
    }

    return totalTime;
  }

  private getAverageBuildTime(project: string): number {
    // En producción, 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 cuáles pueden correr en 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 proyectos que pueden ser buildados en paralelo
    // Proyectos en el mismo grupo no tienen dependencias entre sí
    const groups: string[][] = [];
    const processed = new Set<string>();

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

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

      // Encuentra proyectos que pueden correr 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 en CI/CD
const analyzer = new DependencyAnalyzer(projectGraph);

const changedFiles = getChangedFilesSinceLastCommit();
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}`);

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

¿Cuándo Usar Monorepos?

Monorepos no son para todos los casos. Usa cuando:

Múltiples proyectos relacionados: Frontend + Backend + Mobile compartiendo código
Frecuente sharing de código: Components, utils, types
Refactorización cross-project: Cambios que afectan múltiples proyectos
CI/CD complejo: Necesita orquestar builds de múltiples proyectos

Evita cuando:

  • Proyecto único simple
  • Equipos completamente independientes
  • Proyectos sin código compartido
  • Infraestructura de CI limitada

Conclusión: El Futuro del Gerenciamiento de Código

Monorepos con Nx o Turborepo se convirtieron en herramienta esencial para proyectos modernos. La capacidad de escalar sin friction, combinar con builds inteligentes y cache distribuido, y tener refactorización atómica los hacen elección obvia para 2025.

Si estás interesado en otras tendencias de arquitectura moderna, ve TypeScript en 2025: Dominando el Mercado, donde exploramos cómo Type Safety se integra perfectamente con monorepos.

¡Vamos a por ello! 🦅

📚 ¿Quieres Profundizar Tus Conocimientos en JavaScript?

Este artículo cubrió monorepos y arquitectura avanzada, pero hay mucho más para explorar en el mundo del desarrollo moderno.

Desarrolladores que invierten en conocimiento sólido y estructurado tienden a tener más oportunidades en el mercado.

Material de Estudio Completo

Si quieres dominar JavaScript de básico a avanzado, preparé un guía completo:

Opciones de inversión:

  • $9.90 USD (pago único)

👉 Conocer el Guía JavaScript

💡 Material actualizado con las mejores prácticas del mercado

Comentarios (0)

Este artículo aún no tiene comentarios 😢. ¡Sé el primero! 🚀🦅

Añadir comentarios