Volver al blog

Monorepos en JavaScript: Guía Completa con Turborepo y Nx en 2025

Hola HaWkers, monorepos se consolidaron como arquitectura preferida para proyectos JavaScript de escala. En 2025, con herramientas como Turborepo y Nx maduras, la adopción explotó en startups y grandes empresas.

¿Ya te encontraste gerenciando múltiples repositorios con código duplicado y dependencias desincronizadas? Monorepos resuelven exactamente estos problemas.

Por Qué Monorepos en 2025

Problemas de Polyrepos

Escenario típico:

repos/
├── frontend-web/
├── frontend-mobile/
├── backend-api/
├── shared-utils/
├── design-system/
└── ... 10+ más repositorios

Dolores:

  • Duplicación de código y configuraciones
  • Sincronizar versiones de dependencias entre repos
  • PRs que afectan múltiples repos
  • Setup de ambiente para nuevos desarrolladores
  • CI/CD fragmentado

Beneficios del Monorepo

monorepo/
├── apps/
│   ├── web/
│   ├── mobile/
│   └── api/
├── packages/
│   ├── ui/
│   ├── utils/
│   └── config/
└── package.json

Ganancias:

  • Código compartido sin publicar en npm
  • Refactorings atómicos (un PR cambia todo)
  • Configuraciones centralizadas
  • Dependencias unificadas
  • CI/CD optimizado con caching

Turborepo: Guía Completa

Turborepo ganó tracción por su simplicidad y velocidad. Es la elección más popular para nuevos monorepos.

Setup Inicial

# Crear nuevo monorepo con Turborepo
npx create-turbo@latest

# O añadir a proyecto existente
npm install turbo --save-dev

Estructura de Proyecto

mi-monorepo/
├── apps/
│   ├── web/                 # Next.js app
│   │   ├── package.json
│   │   └── ...
│   ├── docs/                # Docusaurus
│   │   ├── package.json
│   │   └── ...
│   └── api/                 # Express/Fastify
│       ├── package.json
│       └── ...
├── packages/
│   ├── ui/                  # Componentes compartidos
│   │   ├── package.json
│   │   └── ...
│   ├── utils/               # Funciones utilitarias
│   │   ├── package.json
│   │   └── ...
│   ├── config/              # ESLint, TypeScript configs
│   │   ├── eslint/
│   │   └── typescript/
│   └── database/            # Prisma schema y cliente
│       ├── package.json
│       └── ...
├── turbo.json
├── package.json
└── pnpm-workspace.yaml

Configuración del Turborepo

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    },
    "lint": {
      "dependsOn": ["^build"]
    },
    "test": {
      "dependsOn": ["build"],
      "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "deploy": {
      "dependsOn": ["build", "test", "lint"],
      "outputs": []
    }
  }
}
// package.json (raíz)
{
  "name": "mi-monorepo",
  "private": true,
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "lint": "turbo run lint",
    "test": "turbo run test",
    "format": "prettier --write \"**/*.{ts,tsx,md}\""
  },
  "devDependencies": {
    "turbo": "^2.0.0",
    "prettier": "^3.0.0"
  },
  "packageManager": "pnpm@8.15.0"
}
# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"

Configurando Packages Compartidos

// packages/ui/package.json
{
  "name": "@monorepo/ui",
  "version": "0.0.0",
  "private": true,
  "exports": {
    "./button": "./src/button.tsx",
    "./card": "./src/card.tsx",
    "./input": "./src/input.tsx"
  },
  "devDependencies": {
    "@monorepo/config": "workspace:*",
    "react": "^18.2.0",
    "typescript": "^5.3.0"
  },
  "peerDependencies": {
    "react": "^18.2.0"
  }
}
// packages/ui/src/button.tsx
import { forwardRef } from 'react';

export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ variant = 'primary', size = 'md', className, children, ...props }, ref) => {
    const baseStyles = 'rounded-lg font-medium transition-colors';

    const variants = {
      primary: 'bg-blue-600 text-white hover:bg-blue-700',
      secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
      ghost: 'bg-transparent hover:bg-gray-100',
    };

    const sizes = {
      sm: 'px-3 py-1.5 text-sm',
      md: 'px-4 py-2 text-base',
      lg: 'px-6 py-3 text-lg',
    };

    return (
      <button
        ref={ref}
        className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
        {...props}
      >
        {children}
      </button>
    );
  }
);

Button.displayName = 'Button';

Usando Package en App

// apps/web/package.json
{
  "name": "web",
  "version": "0.0.0",
  "private": true,
  "dependencies": {
    "@monorepo/ui": "workspace:*",
    "@monorepo/utils": "workspace:*",
    "next": "^14.0.0",
    "react": "^18.2.0"
  }
}
// apps/web/app/page.tsx
import { Button } from '@monorepo/ui/button';
import { formatDate } from '@monorepo/utils';

export default function HomePage() {
  return (
    <main>
      <h1>Mi App</h1>
      <p>Fecha: {formatDate(new Date())}</p>
      <Button variant="primary" onClick={() => alert('Clicked!')}>
        Haz clic aquí
      </Button>
    </main>
  );
}

Nx: Guía Completa

Nx es más robusto y ofrece más features out-of-box. Ideal para equipos grandes y monorepos complejos.

Setup Inicial

# Crear nuevo monorepo con Nx
npx create-nx-workspace@latest

# Opciones:
# - integrated: Nx gerencia todo
# - package-based: Similar a Turborepo
# - standalone: Single app con Nx

# Añadir a proyecto existente
npx nx@latest init

Estructura de Proyecto Nx

nx-monorepo/
├── apps/
│   ├── web/
│   ├── web-e2e/           # E2E tests automáticos
│   └── api/
├── libs/
│   ├── shared/
│   │   ├── ui/
│   │   └── utils/
│   └── feature/
│       ├── auth/
│       └── dashboard/
├── tools/
│   └── generators/        # Generadores customizados
├── nx.json
├── package.json
└── tsconfig.base.json

Configuración del Nx

// nx.json
{
  "$schema": "./node_modules/nx/schemas/nx-schema.json",
  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    "production": [
      "default",
      "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
      "!{projectRoot}/tsconfig.spec.json"
    ],
    "sharedGlobals": []
  },
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"],
      "cache": true
    },
    "test": {
      "inputs": ["default", "^production"],
      "cache": true
    },
    "lint": {
      "inputs": ["default"],
      "cache": true
    }
  },
  "defaultBase": "main"
}

Generators del Nx

Nx tiene generators poderosos para crear código:

# Crear nuevo app React
nx g @nx/react:app my-app

# Crear biblioteca compartida
nx g @nx/react:lib ui --directory=shared

# Crear componente en biblioteca
nx g @nx/react:component button --project=shared-ui

# Mover proyecto
nx g @nx/workspace:move --project=old-lib --destination=new-path

# Remover proyecto
nx g @nx/workspace:remove old-lib

Affected Commands

Una de las features más poderosas del Nx:

# Solo build proyectos afectados por cambios
nx affected:build

# Solo test proyectos afectados
nx affected:test

# Solo lint proyectos afectados
nx affected:lint

# Ver grafo de dependencias
nx graph

# Ver qué proyectos son afectados
nx show affected

Comparativo: Turborepo vs Nx

Tabla Comparativa

Feature Turborepo Nx
Setup inicial Más simple Más complejo
Curva de aprendizaje Baja Media-alta
Generators No tiene Muy poderosos
Affected commands Básico Avanzado
Remote caching Sí (Vercel) Sí (Nx Cloud)
Grafo de dependencias Básico Avanzado
Plugins/Extensibilidad Limitado Muy extensible
Performance Excelente Excelente
Documentación Buena Excelente
Comunidad Creciente Grande

Cuándo Usar Cada Uno

Elija Turborepo si:

  • Equipo pequeño/mediano
  • Quiere setup rápido
  • Prefiere simplicidad sobre features
  • Ya usa Vercel (caching gratuito)
  • Monorepo con pocos proyectos

Elija Nx si:

  • Equipo grande
  • Necesita generators y automación
  • Monorepo complejo con muchos proyectos
  • Quiere grafo de dependencias avanzado
  • Necesita affected commands precisos

Buenas Prácticas para Monorepos

1. Nomenclatura de Packages

// Usar scopes consistentes
{
  "name": "@monorepo/ui",      // Correcto
  "name": "@mycompany/ui",     // Correcto (scope de la empresa)
  "name": "ui",                // Evitar - puede conflictar con npm
  "name": "my-ui-lib"          // Evitar - difícil de identificar origen
}

2. Configuraciones Compartidas

// packages/config/eslint/react.js
module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'plugin:@typescript-eslint/recommended',
  ],
  rules: {
    'react/react-in-jsx-scope': 'off',
    'react/prop-types': 'off',
  },
};

// Uso en apps/web/.eslintrc.js
module.exports = {
  extends: [require.resolve('@monorepo/config/eslint/react')],
};
// packages/config/typescript/base.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true
  }
}

// Uso en apps/web/tsconfig.json
{
  "extends": "@monorepo/config/typescript/nextjs.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

3. Versionamiento

// package.json
{
  "devDependencies": {
    "changeset": "^2.0.0"
  },
  "scripts": {
    "changeset": "changeset",
    "version": "changeset version",
    "release": "changeset publish"
  }
}
# Flujo de cambios
# 1. Hacer cambios en packages
# 2. Crear changeset
npx changeset

# 3. Responder preguntas sobre el cambio
# 4. Antes del release, consumir changesets
npx changeset version

# 5. Publicar (si packages son públicos)
npx changeset publish

4. CI/CD Optimizado

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Necesario para affected commands

      - uses: pnpm/action-setup@v2
        with:
          version: 8

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

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

      # Turborepo con remote cache
      - name: Build
        run: pnpm build
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

      # O con Nx
      - name: Build affected
        run: npx nx affected:build --base=origin/main~1 --head=HEAD

Conclusión

Monorepos son la arquitectura preferida para proyectos JavaScript de escala en 2025. Con Turborepo o Nx, los dolores tradicionales de gerenciar múltiples repositorios desaparecen.

Resumen:

  • Turborepo: Simplicidad y velocidad
  • Nx: Power features y extensibilidad

Ambos son excelentes elecciones. El importante es comenzar y iterar conforme las necesidades del equipo evolucionan.

Si quieres profundizar en arquitectura de proyectos JavaScript, recomiendo que veas otro artículo: Vite vs Webpack en 2025 donde vas a descubrir cuál build tool elegir para tu monorepo.

¡Vamos a por ello! 🦅

Comentarios (0)

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

Añadir comentarios