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ónNx 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-boxVentajas 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@latestVentajas 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=10El 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)
💡 Material actualizado con las mejores prácticas del mercado

