A medida que escala el ecosistema de ingeniería de una organización, el desarrollo rara vez se limita a una sola aplicación web aislada. Emerge de forma natural un conjunto de productos interconectados: un portal público de clientes (B2C), un panel administrativo privado (B2B Admin Dashboard), portales de documentación, páginas de aterrizaje y micrositios de marketing. Alojar estas iniciativas en repositorios polyrepo desconectados genera una duplicación masiva de código: sistemas de diseño (UI Kits), interfaces tipadas de TypeScript, reglas de linter y utilidades de negocio se replican manualmente, y modificar un simple botón exige decenas de pull requests sincronizados.
Un Monorrepositorio (Monorepo) resuelve esta sobrecarga consolidando el código en un espacio unificado. Los paquetes internos se vinculan directamente mediante protocolos de workspace local sin requerir publicaciones intermedias en registros públicos o privados de npm.
Sin embargo, los monorrepositorios sin herramientas avanzadas de orquestación sufren una degradación severa en tiempos de compilación: modificar un archivo markdown en la documentación no debe forzar la recompilación del portal principal ni del panel administrativo. Para orquestar tareas, calcular grafos de dependencias y gestionar cachés de forma inteligente se emplea Turborepo. Turborepo suprime el cómputo redundante mediante hashes criptográficos y Remote Caching.
En una arquitectura de monorrepositorio basada en pnpm Workspaces, rige una estricta segregación de responsabilidades:
apps/* — Consumidores terminales (aplicaciones Next.js). Importan librerías compartidas, pero ningún otro proyecto importa código proveniente de apps/.packages/* — Librerías compartidas (componentes de UI, esquemas de dominio, configuraciones globales, clientes de base de datos).RAÍZ DEL MONORREPOSITORIO
┌─────────────────────────────────────────────────┐
│ pnpm-workspace.yaml | turbo.json | package.json │
└────────────────────────┬────────────────────────┘
│
┌───────────────────────┴───────────────────────┐
▼ ▼
[apps/ (Consumidores)] [packages/ (Librerías)]
┌───────────────────────────────┐ ┌─────────────────────────────────┐
│ apps/web (Next.js 15 App) │ │ packages/ui (Design System) │
│ apps/admin (Next.js Admin) │ ────────────► │ packages/types (Interfaces) │
│ apps/docs (Documentación) │ workspace:* │ packages/tsconfig (Config TS) │
└───────────────────────────────┘ │ packages/database (Prisma/ORM) │
│ └─────────────────────────────────┘
│ turbo prune ▲
▼ │
┌───────────────────────────────┐ │
│ Docker OCI Image (@acme/web) │ ──────────────────────────────┘
│ (Contiene solo web + ui) │
└───────────────────────────────┘Creación del componente Modal en packages/ui exponiéndolo vía exports en package.json.
Inclusión de '@acme/ui': 'workspace:*' en las dependencias de las aplicaciones que lo consumen.
Añadir el paquete a transpilePackages en next.config.ts e incluir @source en globals.css.
Comprobación de la topología de tareas en turbo.json y validación paralela con pnpm turbo check-types.
Ejecución de pnpm turbo build almacenando artefactos en la Remote Cache.
Un equipo implementa un UI Kit en packages/ui dentro de un monorrepositorio con pnpm. Un desarrollador añade react a la sección dependencies de packages/ui/package.json. Al importar componentes en Next.js 15, la aplicación colapsa con 'Invalid Hook Call: Hooks can only be called inside the body of a function component'. ¿Por qué se produce el fallo y cómo se solventa?
pnpm-workspace.yaml#En la raíz del proyecto, pnpm-workspace.yaml delimita los paquetes gestionados:
packages:
- "apps/*"
- "packages/*"turbo.json#En el estándar de Turborepo 2.x, el bloque principal se denomina tasks (en sustitución del antiguo pipeline), y las variables de entorno se declaran de forma explícita para prevenir la corrupción de la caché:
{
"$schema": "https://turbo.build/schema.json",
"ui": "tui",
"globalEnv": ["NODE_ENV", "VERCEL_ENV"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"],
"env": ["NEXT_PUBLIC_API_URL", "DATABASE_URL"]
},
"lint": {
"dependsOn": ["^lint"]
},
"check-types": {
"dependsOn": ["^check-types"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}En packages/ui, React se declara estrictamente en peerDependencies para prevenir instancias duplicadas:
{
"name": "@acme/ui",
"version": "0.0.0",
"private": true,
"exports": {
".": "./src/index.ts"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"react": "^19.0.0",
"typescript": "^5.7.0"
}
}La vinculación en una aplicación (apps/web/package.json) se formaliza mediante el protocolo workspace:*:
{
"name": "@acme/web",
"version": "0.1.0",
"private": true,
"dependencies": {
"@acme/ui": "workspace:*",
"next": "^15.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}transpilePackages#Debido a que @acme/ui suministra archivos TypeScript sin compilar, el empaquetador de Next.js (Turbopack) los compila en caliente:
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
transpilePackages: ['@acme/ui'],
};
export default nextConfig;En lugar de duplicar opciones de compilador en cada subproyecto, se crea un paquete base en packages/typescript-config/nextjs.json:
{
"extends": "@acme/typescript-config/nextjs.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"]
}En Tailwind CSS v4, el rastreo de clases en paquetes compartidos se realiza mediante la directiva @source en la hoja de estilos principal:
@import "tailwindcss";
/* Indicamos al compilador de Tailwind v4 que rastree los componentes del paquete compartido */
@source "../../../../packages/ui/src/**/*.tsx";En el pipeline de CI/CD de un monorrepositorio, construir la imagen Docker para apps/web tarda 12 minutos. Además, tras actualizar NEXT_PUBLIC_API_URL en el archivo .env, los contenedores en producción continúan realizando peticiones a la API anterior de pruebas. ¿Cómo se solucionan ambos problemas con Turborepo?
Si @acme/ui incluye react en dependencies, el gestor instalará una copia aislada en packages/ui/node_modules. Durante la ejecución en Next.js se producirá un conflicto entre dos instancias distintas de React, provocando la caída de hooks como useState o useEffect.
Solución: Declare react y react-dom en paquetes compartidos únicamente en peerDependencies, fijando una versión única en el package.json raíz mediante overrides de pnpm:
{
"pnpm": {
"overrides": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
}Si la aplicación utiliza NEXT_PUBLIC_API_URL, pero no está declarada en el array tasks.build.env de turbo.json, Turborepo no incluirá su valor al calcular el hash de la tarea. Al cambiar la URL en .env, el compilador asumirá erróneamente un acierto de caché (cache hit) y servirá un build con la URL desfasada.
Solución: Declare siempre todas las variables de entorno que afecten al build en tasks.build.env en turbo.json.
Compilar la imagen de apps/web con COPY . . arrastra aplicaciones ajenas (apps/admin, apps/docs), inflando el tamaño del contenedor en varios gigabytes e invalidando las capas de caché ante cambios irrelevantes.
Solución: Emplee turbo prune @acme/web --docker en la fase de preparación para extraer un corte limpio y autosuficiente.
Las aplicaciones (apps/*) son consumidores finales. Importar utilidades o componentes directamente desde apps/admin hacia apps/web destruye el despliegue autónomo y genera dependencias circulares. Cualquier código reutilizable debe ubicarse en packages/.