Por qué un monorepo

Cuando un proyecto crece, la pregunta no es si necesitas un monorepo, sino cuando. Senales de que es momento:

  • Tienes multiples apps que comparten código (web, admin, mobile)
  • Copias archivos entre repositorios
  • Los cambios en una libreria requieren actualizar varios repos
  • Tu equipo crece y necesitas boundaries claros entre módulos

Un monorepo no significa "todo en un archivo". Significa multiples proyectos en un repositorio con herramientas que los conectan inteligentemente.

Por qué Nx para Angular

Nx es el toolkit de monorepo más maduro para el ecosistema Angular. Fue creado por ex-miembros del equipo de Angular en Google y ofrece:

  • Cache inteligente: No rebuilds cosas que no cambiaron
  • Grafo de dependencias: Visualiza las relaciones entre proyectos
  • Generadores: Scaffolding de apps, librerias y componentes
  • Affected commands: Solo ejecuta tests/builds de lo que cambio
  • Plugins oficiales: Angular, React, Node, Nest, y más

Paso 1: Crear el workspace

Usa el primer bloque de código para crear tu workspace. El preset angular-monorepo configura todo lo necesario para Angular con las mejores prácticas de Nx.

Estructura del workspace

Despues de la creación, tu workspace se ve así:

mi-workspace/
  apps/
    web-app/
      src/
        app/
        assets/
        index.html
        main.ts
        styles.css
      project.json
      tsconfig.app.json
  libs/
  nx.json
  tsconfig.base.json
  package.json

La separacion fundamental es:

  • apps/: Aplicaciones desplegables. Deben ser delgadas (solo bootstrapping y routing)
  • libs/: Librerias reutilizables. Aqui vive la lógica real

Paso 2: Crear librerias

Las librerias son el corazon de un monorepo Nx. Sigue una taxonomia clara. El segundo bloque de código muestra como crear diferentes tipos.

Taxonomia de librerias

Organiza tus librerias en estas categorias:

Tipo Proposito Ejemplo
feature Funcionalidad completa con routing libs/features/courses
ui Componentes de UI reutilizables libs/shared/ui
data-access Servicios, state management libs/shared/data-access
útil Funciones puras, helpers libs/shared/utils
models Interfaces, types, enums libs/shared/models

Path aliases

Nx configura automaticamente path aliases en tsconfig.base.json:

{
  "compilerOptions": {
    "paths": {
      "@mi-workspace/shared/ui": ["libs/shared/ui/src/index.ts"],
      "@mi-workspace/shared/models": ["libs/shared/models/src/index.ts"],
      "@mi-workspace/shared/utils": ["libs/shared/utils/src/index.ts"],
      "@mi-workspace/features/courses": ["libs/features/courses/src/index.ts"]
    }
  }
}

Esto permite importar entre librerias de forma limpia:

import { ButtonComponent } from '@mi-workspace/shared/ui';
import { Course } from '@mi-workspace/shared/models';
import { slugify } from '@mi-workspace/shared/utils';

Paso 3: Mover lógica a librerias

La regla de oro: las apps deben ser delgadas. Toda la lógica se mueve a librerias. Tu app.routes.ts solo conecta features:

import { Routes } from '@angular/router';

export const appRoutes: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('@mi-workspace/features/home')
        .then(m => m.HomeComponent),
  },
  {
    path: 'cursos',
    loadChildren: () =>
      import('@mi-workspace/features/courses')
        .then(m => m.coursesRoutes),
  },
  {
    path: 'blog',
    loadChildren: () =>
      import('@mi-workspace/features/blog')
        .then(m => m.blogRoutes),
  },
];

Paso 4: Boundaries y reglas

Module boundaries

Nx permite definir reglas sobre que librerias pueden importar de cuales. Configura tags en cada project.json:

{
  "name": "features-courses",
  "tags": ["scope:courses", "type:feature"]
}

Y reglas en .eslintrc.json:

{
  "rules": {
    "@nx/enforce-module-boundaries": [
      "error",
      {
        "depConstraints": [
          {
            "sourceTag": "type:feature",
            "onlyDependOnLibsWithTags": ["type:ui", "type:data-access", "type:útil", "type:models"]
          },
          {
            "sourceTag": "type:ui",
            "onlyDependOnLibsWithTags": ["type:útil", "type:models"]
          },
          {
            "sourceTag": "type:útil",
            "onlyDependOnLibsWithTags": ["type:models"]
          },
          {
            "sourceTag": "type:models",
            "onlyDependOnLibsWithTags": []
          }
        ]
      }
    ]
  }
}

Esto garantiza que:

  • Features pueden usar UI, data-access, utils y models
  • UI solo puede usar utils y models
  • Utils solo puede usar models
  • Models no depende de nada

Por qué importan las boundaries

Sin boundaries, un monorepo se convierte rapidamente en un espagueti. Las dependencias circulares aparecen, los cambios en una libreria rompen todo, y el grafo se vuelve inmanejable. Las boundaries previenen esto desde el primer dia.

Paso 5: Cache y affected

Cache local

Nx cachea los resultados de build, test y lint. Si ejecutas nx build web-app y nada cambio, el resultado se sirve de cache instantaneamente:

# Primera vez: build completo
npx nx build web-app
# => 45 segundos

# Segunda vez sin cambios: cache hit
npx nx build web-app
# => 0.3 segundos (desde cache)

Nx Cloud (cache remota)

Nx Cloud permite compartir cache entre desarrolladores y CI:

npx nx connect

Cuando un companero ya buildeo una libreria, tu obtienes el resultado de cache remota sin compilar. En equipos grandes, esto ahorra horas de CI al dia.

Affected commands

Los comandos affected solo ejecutan tareas para los proyectos impactados por los cambios recientes:

# Solo testea lo que cambio
npx nx affected -t test

# Solo buildea lo que cambio
npx nx affected -t build

# Solo lintea lo que cambio
npx nx affected -t lint

Paso 6: CI optimizado

GitHub Actions con Nx

name: CI
on: [push, pull_request]

jobs:
  main:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: actions/setup-node@v4
        with:
          node-versión: 22
          cache: npm

      - run: npm ci

      - uses: nrwl/nx-set-shas@v4

      - run: npx nx affected -t lint test build

El step nx-set-shas determina automaticamente que cambio entre el commit actual y el anterior, para que affected funcione correctamente.

Paso 7: Agregar una segunda app

La ventaja del monorepo se materializa cuando agregas otra app:

npx nx generate @nx/angular:application admin-panel \
  --directory=apps/admin-panel \
  --routing=true \
  --style=css

Esta nueva app puede importar las mismas librerias compartidas:

// En admin-panel/src/app/app.routes.ts
import { ButtonComponent } from '@mi-workspace/shared/ui';
import { Course } from '@mi-workspace/shared/models';

Cambias el componente de UI una vez y ambas apps se actualizan.

Visualizar el grafo

Nx tiene una herramienta de visualización integrada:

npx nx graph

Esto abre un navegador con el grafo interactivo de dependencias. Puedes ver que libreria depende de cual, identificar dependencias circulares y entender el impacto de un cambio.

Errores comunes

Librerias demasiado granulares

No crees una libreria por componente. Agrupa por dominio. Una libreria shared/ui con 15 componentes es mejor que 15 librerias de un componente cada una.

Apps con lógica

Si tu app tiene más de routes y bootstrapping, estas haciendo algo mal. Mueve esa lógica a una libreria feature.

No configurar boundaries

Un monorepo sin boundaries es peor que repos separados. Configura las reglas desde el dia uno.

Ignorar el cache

Si tu CI no usa Nx cache o Nx Cloud, estas desperdiciando la ventaja principal del monorepo.

Conclusion

Nx transforma la gestion de proyectos Angular a nivel profesional. El cache inteligente, los affected commands y las module boundaries te dan estructura y velocidad que seria muy difícil de lograr con herramientas caseras.

Empieza con una app y unas pocas librerias. A medida que tu proyecto crece, el monorepo escala contigo sin convertirse en un caos.