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.
Receta
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.
Ejemplo Funcional
# 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:
- Protocolo
workspace:*enlazandoapps/webapackages/ui - Campo
exportsexponiendo importaciones de subrutas como@repo/ui/button transpilePackagespermitiendo que Next.js compile TSX sin procesar desde paquetes del workspace- Grafo de tareas
turbo.jsoncon ordenamiento topológico^build - Configuración compartida de TypeScript y ESLint consumida como deps del workspace
Análisis Profundo
Cómo Funciona
- Turborepo lee
turbo.jsonpara construir un grafo de tareas en cada workspace. ^buildsignifica "construir todas las dependencias internas antes de esta tarea" — por lo quepackages/uise construye (o está listo) antes deapps/web.- Turbo hashea entradas (archivos de código fuente, lockfile, variables de entorno listadas en
env) y salta tareas cuyo hash no ha cambiado — este es el caché local. pnpmresuelveworkspace:*a un enlace simbólico dentro denode_modules, por lo que los edits enpackages/uison visibles instantáneamente enapps/web.transpilePackagesennext.config.tsle dice a Next.js que@repo/uienvía código fuente, no JS compilado, y debe pasar por SWC.- Con Turbopack (
next dev --turbopack), la recarga en caliente funciona a través de límites de paquete sin observadores adicionales.
Variaciones
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 npmNotas de TypeScript
// 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".
Trampas
-
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, reemplazaworkspace:*con*y confía en el enlace de workspace integrado de npm. -
Fallos de caché de Turbo por lockfile inconsistente — si
pnpm-lock.yamlcambia en CI pero no localmente (o viceversa), cada tarea se vuelve a ejecutar. Solución: confirma el lockfile, usapnpm install --frozen-lockfileen CI, y mantén el campopackageManageren rootpackage.jsonfijado. -
Variables de entorno no pasando a todas las tareas — Turborepo hashea variables de entorno que explícitamente listas. Una variable no declarada en
turbo.jsonenvse elimina de la tarea, causando misterioso "undefined" en tiempo de construcción. Solución: añade cada variable requerida al arrayenvpara 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 desdenode_modulespor defecto, por lo que edits a@repo/uio se bloquean o no se actualizan. Solución: añade el paquete atranspilePackagesennext.config.ts. Alternativamente, construye el paquete a JS y envía una carpetadist/. -
Dependencias circulares entre paquetes —
@repo/uiimportando desde@repo/utilsque importa desde@repo/uisilenciosamente romperá el grafo de tareas de Turbo y causará fallos en tiempo de ejecución. Solución: ejecutapnpm turbo run build --drypara inspeccionar el grafo, y usa un diseño de paquete en capas (paquetes hoja sin deps internos en la parte inferior). -
Campo
exportseclipsandomain— 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.
Alternativas
| 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 |
FAQs
¿Qué significa workspace:* en una dependencia de package.json?
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."
¿Por qué necesito transpilePackages en next.config.ts para @repo/ui?
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.
¿Cuál es la diferencia entre dependsOn: ["^build"] y dependsOn: ["build"]?
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).
¿Cómo ejecuto una tarea solo para una aplicación y sus dependencias?
Usa el filtro con puntos suspensivos finales: pnpm turbo run build --filter=web.... Esto incluye web más cada workspace del que depende.
¿Cómo decide Turborepo si usar el caché?
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.
¿Cómo habilito almacenamiento en caché remoto en Vercel?
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.
Trampa: mi variable de entorno funciona localmente pero es undefined durante turbo run build. ¿Por qué?
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.
Trampa: la recarga en caliente se detuvo después de añadir una nueva exportación a @repo/ui. ¿Por qué?
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.
TypeScript: ¿cómo comparto una base tsconfig.json en todas las aplicaciones?
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.
TypeScript: ¿cómo uso rutas del workspace como @/components/* junto con paquetes del workspace?
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.
¿Puedo usar npm en lugar de pnpm con Turborepo?
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.
¿Cómo añado una segunda aplicación Next.js sin duplicar configuración?
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.
Relacionado
- Environment Variables — manejo de variables de entorno en workspaces
- Deployment — despliegue de aplicaciones de monorepo a Vercel
- ESLint Setup — compartir configuración ESLint como un paquete del workspace
- SaaS Starter — stack full-stack Next.js 15 SaaS