Lección 3 de 310 min
Monorepos con Turborepo
Varios proyectos en un repositorio: workspaces, el grafo de tareas y la caché que evita repetir trabajo.
Tu empresa tiene una tienda web, un panel de administración y una librería de componentes que usan los dos. Cada uno vive en su propio repositorio. Para cambiar el color de un botón, modificas la librería, publicas una versión nueva en npm, actualizas la dependencia en la tienda, luego en el panel… y rezas para que nadie se quede con la versión vieja.
Un monorepo junta todos esos proyectos en un único repositorio, de modo que un cambio en la librería se ve al momento en las apps que la usan. En esta lección verás cómo se organiza uno con los workspaces de tu gestor de paquetes y cómo Turborepo evita que, al crecer, cada build tarde una eternidad.
Uno o muchos repositorios
Monorepo frente a polyrepo
Todos los proyectos relacionados en un repositorio, en lugar de uno por proyecto.
- Lo contrario:
- Polyrepo
En un polyrepo cada proyecto tiene su repositorio, su CI y sus versiones. En un monorepo conviven varias apps y paquetes compartidos, normalmente en carpetas como apps/ y packages/. Empresas como Google o Meta trabajan con monorepos enormes, pero el patrón también encaja en equipos pequeños.
La gran ventaja son los cambios atómicos: en un solo commit modificas la librería y todas las apps que la usan, y el CI lo prueba todo junto. También compartes configuración (TypeScript, ESLint) y es más fácil que todos usen las mismas versiones de las dependencias.
Úsalo cuando
Cuando varios proyectos comparten código y cambian a la vez con frecuencia: un frontend y su librería de componentes, o una API y el cliente que la consume.
Ten en cuenta
Un monorepo no es un monolito: cada app puede desplegarse por separado. Sus costes son otros: sin herramientas, el CI compila y prueba todo en cada cambio, y es más difícil dar permisos por proyecto. Si dos proyectos no comparten nada, juntarlos no aporta.
tienda/
├── apps/
│ ├── web/ → la tienda (Next.js)
│ └── admin/ → el panel (Vite)
├── packages/
│ ├── ui/ → componentes compartidos
│ └── config/ → configuración común de TypeScript y ESLint
├── package.json
├── pnpm-workspace.yaml
├── pnpm-lock.yaml → un solo lockfile para todo
└── turbo.jsonWorkspaces
El gestor de paquetes trata cada carpeta como un paquete y los enlaza entre sí.
- pnpm:
- pnpm-workspace.yaml
- npm:
- campo workspaces
Los workspaces son la función de npm, pnpm y Yarn que hace posible un monorepo. Declaras en la raíz qué carpetas son paquetes y, con un solo install, el gestor instala las dependencias de todos con un único lockfile y enlaza los paquetes locales: apps/web importa @tienda/ui directamente desde packages/ui, sin publicarlo.
En pnpm se declaran en pnpm-workspace.yaml y la dependencia se escribe con el protocolo workspace:*, que significa «la versión que haya en este repositorio». En npm se declaran en el campo workspaces del package.json raíz.
Úsalo cuando
En cualquier monorepo de JavaScript, aunque no uses ninguna otra herramienta. Ya te dan instalación conjunta y comandos por paquete, como pnpm --filter web dev.
Ten en cuenta
Los workspaces enlazan paquetes, pero no deciden en qué orden compilarlos ni evitan repetir trabajo: si web necesita que ui esté compilada antes, esa coordinación es lo que añade Turborepo.
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
// apps/web/package.json
{
"name": "web",
"dependencies": {
"@tienda/ui": "workspace:*",
"next": "^16.0.0"
}
}Nota
Si usas npm, el package.json raíz lleva "workspaces": ["apps/*", "packages/*"] y los comandos de un paquete se lanzan con npm run build --workspace=web. npm no entiende el protocolo workspace:; basta con poner la versión del paquete local o *.
Turborepo
Turborepo es una herramienta de Vercel, escrita en Rust, que se coloca encima de los workspaces. No sustituye a tu gestor de paquetes ni a tu bundler: decide qué tareas ejecutar, en qué orden y cuáles se pueden saltar porque ya se hicieron antes.
Tareas y su grafo: turbo.json
Describe las tareas una vez y Turborepo las ejecuta en orden y en paralelo.
- Creado por:
- Vercel
- Escrito en:
- Rust
En turbo.json declaras las tareas (build, test, lint…) y de qué dependen. La clave es dependsOn: "dependsOn": ["^build"] significa «antes de compilar este paquete, compila los paquetes de los que depende». El ^ apunta a las dependencias; sin él, "dependsOn": ["build"] se refiere a otra tarea del mismo paquete.
Con eso, Turborepo construye un grafo de tareas. Al ejecutar turbo run build, compila primero packages/ui y después, a la vez, apps/web y apps/admin, aprovechando todos los núcleos del procesador. Cada paquete sigue definiendo en su package.json qué hace exactamente su script build.
Úsalo cuando
Cuando un monorepo tiene paquetes que dependen unos de otros y el CI empieza a ser lento. Para dos paquetes sin relación, los workspaces bastan.
Ten en cuenta
Las tareas que no terminan, como dev, deben marcarse con persistent: true y cache: false. La alternativa más conocida es Nx, con más funciones, como generadores de código y reglas sobre qué paquete puede importar a cuál.
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"test": {
"dependsOn": ["build"]
},
"lint": {},
"dev": {
"cache": false,
"persistent": true
}
}
}Caché local y remota
Si las entradas no han cambiado, el resultado tampoco: se recupera en lugar de recalcular.
- Clave:
- Hash de las entradas
- Remota:
- Opcional
Antes de ejecutar una tarea, Turborepo calcula un hash (una huella) de todo lo que puede influir en ella: los archivos del paquete, sus dependencias, la configuración de la tarea y las variables de entorno que declares. Si ese hash ya se vio antes, no ejecuta nada: restaura los archivos de outputs y repite los mensajes de la terminal. Si todas las tareas aciertan, verás el mensaje «FULL TURBO».
Por defecto la caché es local, en tu equipo. Con la caché remota se comparte entre todo el equipo y el CI: si un compañero ya compiló ese mismo código, el CI y el resto del equipo descargan el resultado en lugar de compilarlo de nuevo. Vercel ofrece una, y también se puede alojar en tu propia infraestructura.
Úsalo cuando
Siempre que uses Turborepo; la caché local viene activada. La remota compensa en cuanto varias personas o el CI compilan el mismo código.
Ten en cuenta
Si olvidas declarar las outputs, habrá acierto de caché pero no aparecerá la carpeta dist. Y si el build depende de una variable de entorno que no declaras en env, Turborepo puede reutilizar un resultado compilado con otro valor.
$ turbo run build
Tasks: 3 successful, 3 total
Cached: 0 cached, 3 total
Time: 48.2s
# Sin tocar nada, otra vez:
$ turbo run build
Tasks: 3 successful, 3 total
Cached: 3 cached, 3 total
Time: 190ms >>> FULL TURBO
# Comparte la caché con el equipo y el CI
$ turbo login && turbo linkFiltrado y paquetes afectados
Ejecutar las tareas solo en los paquetes que importan para este cambio.
- Opción:
- --filter
Con --filter eliges sobre qué paquetes trabajar: --filter=web solo la tienda; --filter=web... la tienda y todo lo que necesita; --filter=...ui la librería y todo lo que depende de ella, para comprobar que tu cambio no rompe a nadie.
También puedes filtrar por cambios en Git: --filter=[main] selecciona los paquetes modificados respecto a la rama main, y en las versiones recientes turbo run test --affected hace lo mismo con los paquetes afectados de forma automática. pnpm admite una sintaxis de --filter muy parecida.
Úsalo cuando
En el CI de un monorepo grande, para que una pull request que solo toca el panel no pruebe toda la tienda, y en local para arrancar solo la app en la que trabajas.
Ten en cuenta
Filtrar por cambios solo es fiable si el grafo de dependencias está bien declarado. Si una app usa un paquete sin listarlo en su package.json, Turborepo no sabrá que debe probarla cuando ese paquete cambie.
turbo run dev --filter=web # solo la tienda
turbo run build --filter=web... # la tienda y sus dependencias
turbo run test --filter=...ui # ui y quien depende de ella
turbo run lint --filter=[main] # lo que cambió respecto a main
turbo run test --affected # versiones recientes
pnpm --filter admin add zod # pnpm usa la misma ideaAtención
La caché solo es segura si las tareas son deterministas: mismas entradas, mismo resultado. Un build que mete la fecha actual o lee un archivo que no está declarado como entrada puede dar resultados distintos a los que se guardaron en caché.
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
Para cambiar el texto de un botón compartido tienes que tocar tres repositorios, publicar una versión nueva de la librería y actualizarla a mano en dos apps.
Para recordar
- Un monorepo junta varios proyectos en un repositorio para hacer cambios atómicos; no es un monolito, porque cada app se despliega por separado.
- Los workspaces de npm, pnpm o Yarn instalan todo de una vez y enlazan los paquetes locales sin publicarlos.
- En Turborepo,
"dependsOn": ["^build"]significa «compila antes mis dependencias»; con eso construye el grafo de tareas. - La caché funciona por hash de las entradas: si nada cambió, recupera las
outputsen lugar de recalcular, y la remota lo comparte con el equipo y el CI. --filtery--affectedejecutan tareas solo en los paquetes relevantes para un cambio.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.Tu equipo duda en pasar a un monorepo porque «no queremos un monolito». ¿Qué les respondes?
2.En
turbo.json, ¿qué significa"dependsOn": ["^build"]en la tareabuild?3.¿Cómo decide Turborepo que puede saltarse una tarea?
4.Turborepo dice que la tarea
buildacertó en la caché, pero la carpetadistno aparece. ¿Qué es lo más probable?5.Has cambiado
packages/uiy quieres probar todo lo que podría romperse. ¿Qué filtro usas?