Next.js en un Monorepo de Turborepo
Configura Next.js 15 dentro de un monorepo de Turborepo con componentes UI compartidos, configuración TypeScript compartida y compilaciones paralelas en aplicaciones y paquetes.
Busca en todas las páginas de la documentación
Configura Next.js 15 dentro de un monorepo de Turborepo con componentes UI compartidos, configuración TypeScript compartida y compilaciones paralelas en aplicaciones y paquetes.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tarjeta de referencia rápida - lista para copiar y pegar.
# Crea un nuevo Turborepo con la plantilla predeterminada de Next.js + library
npx create-turbo@latest my-monorepo
# Elige "pnpm" cuando se te lo pida (recomendado para soporte del protocolo workspace)
cd my-monorepo
pnpm install
# Ejecuta todas las aplicaciones en modo dev en paralelo
pnpm dev
# Construye todo (almacenado en caché)
pnpm build
# Ejecuta una tarea para un solo workspace
pnpm turbo run dev --filter=webLa plantilla predeterminada te proporciona:
my-monorepo/
apps/
web/ # Aplicación Next.js 15 + React 19
docs/ # Segunda aplicación Next.js (opcional)
packages/
ui/ # Biblioteca de componentes React compartida
eslint-config/ # Configuración ESLint compartida
typescript-config/# Archivos base tsconfig compartidos
turbo.json # Pipeline / grafo de tareas
pnpm-workspace.yaml # Definiciones de workspace
package.json # Scripts raíz + devDependencies
Cuándo usarlo: Cuando tienes más de una aplicación Next.js, un sistema de diseño compartido o utilidades compartidas que deben versionarse y compilarse juntas, y deseas compilaciones rápidas almacenadas en caché en CI.
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"// package.json (root)
\{
"name": "my-monorepo",
"private": true,
"scripts": \{
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"typecheck": "turbo run typecheck",
"clean": "turbo run clean && rm -rf node_modules"
\},
"devDependencies": \{
"turbo": "^2.3.0",
"typescript": "^5.6.3",
"prettier": "^3.3.3"
\},
"packageManager": "pnpm@9.12.0",
"engines": \{
"node": ">=20"
\}
\}// turbo.json
\{
"$schema": "https://turbo.build/schema.json",
"ui": "tui",
"tasks": \{
"build": \{
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"],
"env": ["NODE_ENV", "NEXT_PUBLIC_*"]
\},
"dev": \{
"cache": false,
"persistent": true
\},
"lint": \{
"dependsOn": ["^lint"]
\},
"typecheck": \{
"dependsOn": ["^typecheck"]
\},
"clean": \{
"cache": false
\}
\}
\}// apps/web/package.json
\{
"name": "web",
"version": "0.1.0",
"private": true,
"scripts": \{
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"typecheck": "tsc --noEmit"
\},
"dependencies": \{
"@repo/ui": "workspace:*",
"next": "15.1.0",
"react": "19.0.0",
"react-dom": "19.0.0"
\},
"devDependencies": \{
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/node": "^22.10.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.6.3"
\}
\}// packages/ui/package.json
\{
"name": "@repo/ui",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": \{
"./button": "./src/button.tsx",
"./card": "./src/card.tsx",
"./styles.css": "./src/styles.css"
\},
"scripts": \{
"lint": "eslint . --max-warnings 0",
"typecheck": "tsc --noEmit"
\},
"devDependencies": \{
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/react": "^19.0.0",
"react": "19.0.0",
"typescript": "^5.6.3"
\},
"peerDependencies": \{
"react": "^19.0.0"
\}
\}// packages/ui/src/button.tsx
import type \{ ButtonHTMLAttributes, ReactNode \} from "react";
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> \{
children: ReactNode;
variant?: "primary" | "secondary";
\}
export function Button(\{ children, variant = "primary", ...rest \}: ButtonProps) \{
return (
<button data-variant=\{variant\} \{...rest\}>
\{children\}
</button>
);
\}// apps/web/next.config.ts
import type \{ NextConfig \} from "next";
const nextConfig: NextConfig = \{
// Requerido: Next.js debe transpilar paquetes del workspace que envíen TS/TSX sin procesar
transpilePackages: ["@repo/ui"],
\};
export default nextConfig;// apps/web/app/page.tsx
import \{ Button \} from "@repo/ui/button";
export default function Home() \{
return (
<main>
<h1>Turborepo + Next.js 15</h1>
<Button variant="primary">Haz clic aquí</Button>
</main>
);
\}Lo que esto demuestra:
workspace:* enlazando apps/web a packages/uiexports exponiendo importaciones de subrutas como @repo/ui/buttontranspilePackages permitiendo que Next.js compile TSX sin procesar desde paquetes del workspaceturbo.json con ordenamiento topológico ^buildturbo.json para construir un grafo de tareas en cada workspace.^build significa "construir todas las dependencias internas antes de esta tarea" - por lo que packages/ui se construye (o está listo) antes de apps/web.env) y salta tareas cuyo hash no ha cambiado - este es el caché local.pnpm resuelve workspace:* a un enlace simbólico dentro de node_modules, por lo que los edits en packages/ui son visibles instantáneamente en apps/web.transpilePackages en next.config.ts le dice a Next.js que @repo/ui envía código fuente, no JS compilado, y debe pasar por SWC.next dev --turbopack), la recarga en caliente funciona a través de límites de paquete sin observadores adicionales.Añadir una segunda aplicación Next.js:
# Copia apps/web → apps/docs, cambia el nombre en package.json
cp -r apps/web apps/docs
# Edita apps/docs/package.json: "name": "docs", puerto diferente
pnpm install
pnpm turbo run dev --filter=docsCompartir configuración de Tailwind v4 en aplicaciones:
/* packages/ui/src/styles.css */
@import "tailwindcss";
@theme \{
--color-brand: #2563eb;
\}/* apps/web/app/globals.css */
@import "@repo/ui/styles.css";Ejecutar tareas en paralelo vs en serie:
# Paralelo (predeterminado para tareas independientes)
pnpm turbo run lint typecheck
# Serie mediante dependsOn en turbo.json
# "build": \{ "dependsOn": ["^build", "lint"] \}Comandos con alcance usando --filter:
# Solo construir la aplicación web y sus dependencias
pnpm turbo run build --filter=web...
# Solo lo que ha cambiado desde main
pnpm turbo run build --filter="...[origin/main]"Almacenamiento en caché remoto con Vercel:
pnpm turbo login
pnpm turbo link
# turbo.json - opcional, ya habilitado por defecto cuando está vinculadoChangesets para versionado de paquetes públicos:
pnpm add -Dw @changesets/cli
pnpm changeset init
pnpm changeset # registra un cambio
pnpm changeset version # incrementa versiones
pnpm changeset publish # publica en npm// packages/typescript-config/nextjs.json
\{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Next.js",
"extends": "./base.json",
"compilerOptions": \{
"plugins": [\{ "name": "next" \}],
"module": "ESNext",
"moduleResolution": "Bundler",
"allowJs": true,
"jsx": "preserve",
"noEmit": true
\},
"include": ["src", "next-env.d.ts", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
\}// apps/web/tsconfig.json
\{
"extends": "@repo/typescript-config/nextjs.json",
"compilerOptions": \{
"baseUrl": ".",
"paths": \{
"@/*": ["./*"]
\}
\},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
\}Para un paquete de solo tipos compartido, crea packages/types con "types": "./src/index.ts" y sin código en tiempo de ejecución - cada workspace puede entonces hacer import type \{ User \} from "@repo/types".
workspace:* es una característica de pnpm/yarn - npm no entiende el protocolo workspace. Solución: usa pnpm (recomendado por Turborepo), o Yarn 3+/4, o Bun. Si debes usar npm, reemplaza workspace:* con * y confía en el enlace de workspace integrado de npm.
Fallos de caché de Turbo por lockfile inconsistente - si pnpm-lock.yaml cambia en CI pero no localmente (o viceversa), cada tarea se vuelve a ejecutar. Solución: confirma el lockfile, usa pnpm install --frozen-lockfile en CI, y mantén el campo packageManager en root package.json fijado.
Variables de entorno no pasando a todas las tareas - Turborepo hashea variables de entorno que explícitamente listas. Una variable no declarada en turbo.json env se elimina de la tarea, causando misterioso "undefined" en tiempo de construcción. Solución: añade cada variable requerida al array env para esa tarea (p. ej. "env": ["DATABASE_URL", "NEXT_PUBLIC_*"]).
Recarga en caliente entre paquetes rompe sin transpilePackages - Next.js se niega a compilar TSX sin procesar desde node_modules por defecto, por lo que edits a @repo/ui o se bloquean o no se actualizan. Solución: añade el paquete a transpilePackages en next.config.ts. Alternativamente, construye el paquete a JS y envía una carpeta dist/.
Dependencias circulares entre paquetes - @repo/ui importando desde @repo/utils que importa desde @repo/ui silenciosamente romperá el grafo de tareas de Turbo y causará fallos en tiempo de ejecución. Solución: ejecuta pnpm turbo run build --dry para inspeccionar el grafo, y usa un diseño de paquete en capas (paquetes hoja sin deps internos en la parte inferior).
Campo exports eclipsando main - una vez que añades "exports" a un paquete, cualquier subruta no listada se vuelve inaccesible. Solución: lista cada entrada que necesites explícitamente, incluyendo "./package.json" si los consumidores la leen.
Turbopack y paquetes del workspace - muy viejos builds de Turbopack no seguían symlinks correctamente. Si el servidor dev muestra código anticuado, termínalo, rm -rf apps/web/.next, y reinicia.
| Alternativa | Úsalo Cuando | No lo Uses Cuando |
|---|---|---|
| Nx | Necesitas generadores, visualización de grafo y ecosistema de plugins para repos políglotas | Quieres la herramienta más ligera posible sin configuración |
| Lerna | Mantenimiento de un repo Lerna existente | Comenzar de cero (ahora es un thin wrapper alrededor de Nx) |
| Solo Yarn workspaces | Monorepo simple de solo enlace sin necesidades de caché de tareas | Necesitas caché remoto o características de grafo de tareas |
| Solo pnpm workspaces | Solo necesitas enlace de paquetes, no orquestación de tareas | Ejecutas muchas tareas en CI y quieres almacenamiento en caché |
| Rush (Microsoft) | Monorepos empresariales muy grandes con controles de política estricta | Equipos pequeños - tiene una curva de aprendizaje pronunciada |
| Bun workspaces | Proyectos solo de Bun que quieren la instalación más rápida | Necesitas la madurez y ecosistema de pnpm/Turbo |
Le dice a pnpm (o yarn/bun) que resuelva esta dependencia al paquete de workspace coincidente en el monorepo mediante un symlink, en lugar de descargar de npm. El * significa "cualquier versión actualmente en el workspace."
Next.js ignora TypeScript y JSX en node_modules por defecto. Porque @repo/ui es enlazado simbólicamente en node_modules y envía .tsx sin procesar, debes listarlo en transpilePackages para que el compilador SWC de Next lo procese.
El acento circunflejo ^ significa "la tarea build de mis dependencias del workspace debe ejecutarse primero" (topológico). Sin el acento circunflejo, significa "la tarea build de este mismo workspace debe ejecutarse primero" (dependencia a nivel de tarea dentro del mismo paquete).
Usa el filtro con puntos suspensivos finales: pnpm turbo run build --filter=web.... Esto incluye web más cada workspace del que depende.
Calcula un hash de: entradas de tareas (archivos de código fuente), el lockfile, las vars env declaradas y el grafo de tareas resuelto. Si el hash coincide con una ejecución anterior, reproduce la salida almacenada en caché y los logs en lugar de re-ejecutar.
Ejecuta pnpm turbo login luego pnpm turbo link. Esto escribe un .turbo/config.json apuntando a tu equipo de Vercel y carga artefactos de caché en cada tarea exitosa.
Turborepo elimina variables de entorno no declaradas en el array env (o globalEnv) de turbo.json, porque las variables de entorno no rastreadas envenenarían el caché. Añade el nombre de variable al array env para esa tarea.
Probablemente añadiste el archivo pero olvidaste listarlo en el campo exports de packages/ui/package.json. Una vez que exports está presente, cada subruta debe ser declarada explícitamente.
Crea packages/typescript-config con archivos como base.json y nextjs.json, añádelo como una devDependency del workspace, y usa "extends": "@repo/typescript-config/nextjs.json" en el tsconfig.json de cada aplicación.
Define paths en el tsconfig.json de la aplicación consumidora, no en la base compartida. paths se resuelven relativo a baseUrl, que debe ser el directorio de la aplicación - por lo que cada aplicación Next.js posee su propio bloque paths mientras aún extiende las opciones compartidas del compilador.
Sí, Turborepo soporta npm, yarn, pnpm y bun. Sin embargo, npm no soporta el protocolo workspace:*, por lo que necesitarás usar * o versiones explícitas y perder algunas garantías sobre enlace contra la copia local.
Crea apps/docs, apúntalo a las @repo/typescript-config y @repo/eslint-config compartidas, añade @repo/ui como una dep del workspace, y Turborepo lo descubrirá automáticamente en el próximo pnpm install. Usa --filter=docs para dirigirte a él.
Revisado por Chris St. John·Última actualización: 10 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥