Next.js
Monorepos con Next.js
Explica cómo organizar aplicaciones y paquetes Next.js en un monorepo con workspaces, boundaries, APIs públicas, caché de tareas y ownership claro.
- Última actualización
- Actualizada
- Nivel
- Profundización
Next.js
Explica cómo organizar aplicaciones y paquetes Next.js en un monorepo con workspaces, boundaries, APIs públicas, caché de tareas y ownership claro.
Un monorepo reúne varias aplicaciones y paquetes bajo un mismo repositorio. No es una arquitectura por sí solo: aporta valor cuando necesitas compartir código, coordinar cambios y ejecutar tareas de forma consistente sin mezclar responsabilidades ni boundaries de servidor y cliente.
repository
├─ apps/
│ ├─ web-next
│ ├─ admin-next
│ └─ worker
├─ packages/
│ ├─ ui
│ ├─ config
│ ├─ database
│ ├─ auth
│ └─ domain
└─ task orchestrator
├─ dependency graph
├─ cache
└─ filtered executionEl objetivo no es compartir todo. Es definir qué pertenece a cada app, qué puede reutilizarse y qué nunca debe entrar al grafo cliente.
No suele aportar en una aplicación pequeña con un único equipo y pocas dependencias. Añade tooling, tiempos de CI y complejidad de ownership.
Ejemplo con pnpm:
packages:
- apps/*
- packages/*Root package.json:
{
"private": true,
"packageManager": "pnpm@10.0.0",
"scripts": {
"build": "turbo build",
"lint": "turbo lint",
"test": "turbo test"
}
}Cada workspace declara sus propias dependencias. No confíes en hoisting accidental: un package debe listar todo lo que importa.
{
"name": "@acme/domain-orders",
"exports": {
".": "./src/index.ts",
"./schemas": "./src/schemas.ts"
}
}Usa exports para controlar qué módulos consumen otros paquetes. Evita imports profundos:
import { createOrder } from "@acme/domain-orders/src/internal/service";Eso rompe encapsulación y dificulta cambios.
Clasifica paquetes:
@acme/ui → client-safe
@acme/domain → mostly runtime-neutral
@acme/database → server-only
@acme/auth-server → server-only
@acme/config-public → client-safe valuesEn paquetes servidor:
import "server-only";No exportes desde un mismo barrel componentes cliente, DB y secrets. Un import aparentemente inocente puede arrastrar código servidor al bundle o provocar errores de build.
Un design system puede contener:
Decide si cada componente necesita "use client". No marques el entrypoint completo como cliente si solo unos pocos componentes son interactivos.
Mejor:
@acme/ui/server
@acme/ui/cliento exports específicos por componente.
Next.js puede necesitar transpilar paquetes workspace:
const nextConfig = {
transpilePackages: ["@acme/ui", "@acme/domain"],
};Aporta cuando un package publica TypeScript o sintaxis que debe procesar Next.js. No añadas todos los paquetes por costumbre; aumenta trabajo de build.
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**"]
},
"lint": {},
"test": {
"dependsOn": ["^build"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}^build significa construir dependencias antes. El cache reutiliza outputs solo si inputs relevantes no cambiaron.
Una tarea debe invalidarse cuando cambia una variable que afecta su resultado:
{
"globalEnv": ["NODE_ENV"],
"tasks": {
"build": {
"env": ["NEXT_PUBLIC_SITE_URL", "FEATURE_FLAG"]
}
}
}No incluyas secretos en logs o hashes públicos. Diferencia variables que cambian output de las que solo se leen en runtime.
Permite compartir resultados entre CI y desarrolladores. Riesgos:
Usa namespaces por repo/branch cuando corresponda y tokens de mínimo privilegio.
pnpm turbo build --filter=web...Construye la app y sus dependencias. Es útil para CI y deploy por app.
No filtres solo por carpetas modificadas sin considerar dependencias transitivas; un cambio en packages/ui afecta varias apps.
Opciones:
Todo el repositorio avanza junto. Adecuado para apps desplegadas coordinadamente.
Packages publicados tienen semver y changelog. Aporta cuando existen consumidores externos o releases separados.
No publiques cada package interno a npm si workspaces y despliegue conjunto resuelven el problema.
ui → domain → uiCausan:
Define dirección:
apps
→ features/application
→ domain
→ infrastructure adaptersLa UI puede depender de tipos de dominio; el dominio no debe depender de React.
Centraliza schema/client en un package servidor:
packages/database
├─ schema
├─ migrations
├─ client
└─ test helpersNo permitas que cualquier app ejecute migraciones al iniciar. CI o un job dedicado las coordina.
Un worker y Next.js pueden compartir tipos/casos de uso sin compartir necesariamente el mismo proceso.
Paquetes útiles:
@acme/tsconfig.@acme/eslint-config.@acme/tailwind-config cuando realmente aplica.Evita configuraciones tan rígidas que impidan necesidades de una app. Comparte defaults y permite extensión explícita.
Define:
Un package “shared” sin owner se convierte en un lugar donde todos añaden cosas y nadie puede cambiar nada.
Puedes usar reglas ESLint para prohibir imports server-only desde client-safe packages.
Pipeline conceptual:
changed package
→ determine affected apps
→ lint/test affected graph
→ build previews per app
→ deploy independentlyUna migración compartida exige coordinación entre todas las aplicaciones que consumen el schema.
DomiSys podría evolucionar a:
apps/
├─ web Next.js customer/admin
├─ platform-admin Next.js
└─ worker jobs/imports
packages/
├─ ui
├─ orders
├─ inventory
├─ auth
├─ database
└─ observabilityNo separarías orders en un package solo para tener más carpetas: debe contener reglas compartidas por web y worker.
No expresa ownership ni boundaries.
Arrastra módulos cliente y servidor.
Funcionan por hoisting y fallan en CI.
El coste supera la reutilización.
Reutiliza builds incorrectos.
Impide usarlo desde worker o tests.
Carreras y bloqueos.
transpilePackages se usa con intención.transpilePackages?Pages Router: compatibilidad y legado separa el modelo anterior del App Router para mantener y migrar proyectos sin mezclar APIs.