Cómo crear un monorepo con pnpm y Turborepo para aplicaciones Node.js, paso a paso
Cree un monorepo de TypeScript a partir de una carpeta vacía utilizando pnpm workspaces y Turborepo, y luego ejecute, compile y filtre tareas en una aplicación web, una API y paquetes compartidos.
Una vez que un producto cuenta con una interfaz de usuario, un backend y el código necesario para ambos, los repositorios separados comienzan a ser problemáticos: los tipos compartidos se desvían, la configuración se copia a mano y un solo cambio requiere varios pull requests. pnpm workspaces junto con Turborepo resuelven esto sin fusionar los proyectos en uno solo. Esta guía muestra cómo pasar de un directorio vacío a dos aplicaciones en TypeScript y un paquete compartido que se puede desarrollar, compilar y gestionar desde una única raíz.
El diseño final:
my-monorepo/
├── apps/
│ ├── web/
│ └── api/
│
├── packages/
│ ├── types/
│ └── eslint-config/
│
├── package.json
├── pnpm-workspace.yaml
├── turbo.json
├── tsconfig.json
└── pnpm-lock.yaml
Qué te ofrece un monorepo
Un monorepo es un único repositorio Git que alberga varias aplicaciones y paquetes. La alternativa es tener un repositorio por cada área de funcionalidad:
frontend-repository
backend-repository
shared-types-repository
ui-library-repository
En un monorepo, estas se convierten en carpetas, con el código listo para desplegarse bajo apps y el código reutilizable bajo packages:
my-monorepo/
├── apps/
│ ├── web/
│ └── api/
│
└── packages/
├── types/
└── ui/
El código se comparte directamente en lugar de publicarse primero. Un paquete de tipos consumido tanto por el frontend como por el backend significa que cualquier cambio en el contenido del paquete llega a ambos lados en un único commit:
apps/web
↓
packages/types
↑
apps/api
Dónde encaja Turborepo
Los espacios de trabajo de pnpm vinculan los paquetes; Turborepo decide cómo se ejecutan las tareas en ellos. Ofrece orquestación de tareas, ordenamiento basado en dependencias, ejecución paralela, caché local y remoto, compilaciones incrementales y soporte para espacios de trabajo.
Tomemos un repositorio con tres espacios de trabajo:
apps/web
apps/api
packages/types
Cada uno puede definir las mismas scripts:
build
lint
test
dev
En lugar de ingresar a cada directorio en el orden correcto, se ejecutan desde la raíz y Turborepo paraleliza lo que es posible. Para saber cuándo vale la pena utilizar esta opción, consulte dónde encaja Turborepo en un monorepo de NestJS y cuándo omitirlo.
Requisitos previos
Necesita Node.js, pnpm, Git y un editor. Verifique Node.js:
node -v
Y pnpm:
pnpm -v
Si falta pnpm, Corepack, incluido con Node.js, puede proporcionarlo. Actívelo:
corepack enable
Active la versión más reciente de pnpm:
corepack prepare pnpm@latest --activate
Confirme:
pnpm -v
Configuración del espacio de trabajo raíz
Cree el directorio:
mkdir my-monorepo
cd my-monorepo
Inicialice Git:
git init
Genere el manifiesto raíz:
pnpm init
Lo que deja:
my-monorepo/
└── package.json
Instale Turborepo en la raíz
Turborepo sirve a todo el repositorio, por lo que --workspace-root lo instala en la raíz en lugar de dentro de un paquete:
pnpm add turbo --save-dev --workspace-root
El manifiesto de la raíz termina con scripts que delegan en turbo run; la opción private impide que la raíz sea publicada:
{
"name": "my-monorepo",
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"test": "turbo run test"
},
"devDependencies": {
"turbo": "..."
}
}
La versión de turbo que utiliza depende del momento de instalación. Las versiones recientes también exigen un campo packageManager en el package.json de la raíz; si turbo no puede detectar su gestor de paquetes, consulte la documentación.
Declare los espacios de trabajo
pnpm encuentra los paquetes a través de un archivo de la raíz:
pnpm-workspace.yaml
Enumere los nombres genéricos que deben considerarse como paquetes:
packages:
- "apps/*"
- "packages/*"
Cada directorio directamente debajo de ellos se convierte en un espacio de trabajo:
apps/*
packages/*
Cree las carpetas para ambas aplicaciones y el paquete types:
mkdir -p apps/web
mkdir -p apps/api
mkdir -p packages/types
El árbol hasta ahora:
my-monorepo/
├── apps/
│ ├── web/
│ └── api/
│
├── packages/
│ └── types/
│
├── package.json
└── pnpm-workspace.yaml
Agregar las dos aplicaciones
La aplicación web
Para mantener el enfoque en el monorepo, la aplicación web es simplemente una aplicación Node.js por ahora. Úsela:
cd apps/web
Déle un manifiesto:
pnpm init
La forma final deseada:
apps/web/
├── package.json
└── src/
└── index.ts
Cree el archivo de entrada:
mkdir src
touch src/index.ts
Agregue un marcador de posición:
console.log("Hello from Web application");
Cada espacio de trabajo necesita TypeScript, así que regrese a la raíz:
cd ../..
E instálelo una vez:
pnpm add typescript --save-dev --workspace-root
La API
Mismo patrón: úsela e inicialícela:
cd apps/api
pnpm init
Cree el archivo de entrada:
mkdir src
touch src/index.ts
Agregue su marcador de posición:
console.log("Hello from API application");
Las dos aplicaciones ahora son iguales:
apps/
├── web/
│ ├── src/
│ │ └── index.ts
│ └── package.json
│
└── api/
├── src/
│ └── index.ts
└── package.json
Compartir la configuración de TypeScript
Volver a la raíz:
cd ../..
Crear una configuración base:
tsconfig.json
Contiene opciones que comparten todos los espacios de trabajo: un destino moderno, resolución con NodeNext y verificación estricta:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
Cada aplicación la extiende y agrega solo configuraciones locales. Para la aplicación web, crear:
apps/web/tsconfig.json
Señala al archivo raíz, establece una carpeta de salida y compila solo src:
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"]
}
La API obtiene el mismo archivo:
apps/api/tsconfig.json
Con contenido idéntico:
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"]
}
Los cambios en la estrictitud o el destino ahora ocurren en un solo lugar.
Proporcionar scripts de compilación a cada espacio de trabajo
Turborepo ejecuta los scripts definidos por los espacios de trabajo. Abra el manifiesto web:
apps/web/package.json
Establecer un nombre específico y tres scripts: build mediante tsc, dev en modo de vigilancia, lint mediante ESLint:
{
"name": "@repo/web",
"private": true,
"scripts": {
"build": "tsc",
"dev": "tsx watch src/index.ts",
"lint": "eslint ."
}
}
Luego, el manifiesto de la API:
apps/api/package.json
Con su propio nombre:
{
"name": "@repo/api",
"private": true,
"scripts": {
"build": "tsc",
"dev": "tsx watch src/index.ts",
"lint": "eslint ."
}
}
Los filtros y las dependencias del espacio de trabajo hacen referencia a estos nombres @repo/.... El script dev necesita tsx:
pnpm add tsx --save-dev --workspace-root
El script lint también asume que ESLint está instalado y configurado; agréguelo o elimine el script, de lo contrario pnpm lint fallará.
Configuración de la tubería de tareas
turbo.json describe cómo se comporta cada tarea. Créelo en la raíz:
turbo.json
Defina las tareas:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^lint"]
}
}
}
Qué observar:
"dependsOn": ["^build"]hace que un paquete espere a que se construyan los paquetes del espacio de trabajo de los cuales depende; el signo de apuntación indica dependencias.
outputs indica qué se debe cachear y restaurar, de modo que los paquetes sin cambios no se vuelvan a compilar.dev está sin cachear y es persistent porque un monitor nunca termina.lint también ejecuta primero las dependencias.tasks es la clave actual; las versiones anteriores utilizaban pipeline, por lo que los ejemplos más antiguos podrían necesitar ajustes.
Ejecución y compilación desde la raíz
Inicie todos los procesos de desarrollo:
pnpm dev
Esto funciona porque el script dev de la raíz es:
"dev": "turbo run dev"
Turborepo encuentra dev en cada espacio de trabajo y los inicia juntos, utilizando una única terminal para la aplicación web:
cd apps/web
pnpm dev
Y otra para la API:
cd apps/api
pnpm dev
Con una única orden desde la raíz:
pnpm dev
Compila todo de la misma manera:
pnpm build
Turborepo ordena las compilaciones según las dependencias de los paquetes, comenzando por los paquetes compartidos:
pnpm build
│
▼
turbo run build
│
├── packages/types
│
├── apps/api
│
└── apps/web
Ese orden proviene de las dependencias declaradas: hasta que packages/types cuente con un package.json y las aplicaciones dependan de él, Turborepo no lo compilará primero.
Dirigirse a un solo espacio de trabajo
El parámetro --filter de pnpm ejecuta un script en un espacio de trabajo, como el servidor de desarrollo de la API:
pnpm --filter @repo/api dev
O una compilación web:
pnpm --filter @repo/web build
El propio filtro de Turborepo se encarga del caché y del ordenamiento:
pnpm turbo run build --filter=@repo/api
Comandos que utilizará a diario
Instalar todo:
pnpm install
Iniciar el desarrollo:
pnpm dev
Compilar todo:
pnpm build
Revisar la sintaxis de todo:
pnpm lint
Compilar un solo paquete:
pnpm --filter @repo/api build
Ejecutar una sola aplicación:
pnpm --filter @repo/web dev
Agregar una dependencia a un espacio de trabajo:
pnpm --filter @repo/api add express
Depender de un paquete local; workspace:* enlaza con la copia del repositorio en lugar de descargarla desde el registro:
pnpm --filter @repo/api add @repo/types@workspace:*
¿Por qué no limitarse a los workspaces de pnpm?
Se podría confiar únicamente en los workspaces:
apps/
packages/
No obstante, a medida que el repositorio crece, es necesario coordinar más manualmente:
build
test
lint
typecheck
dev
dependencies
task ordering
caching
Turborepo añade capacidad de orquestación: un único comando comprende las relaciones entre paquetes, omite los trabajos sin cambios y ejecuta tareas independientes en paralelo:
pnpm turbo run build
Para una sola aplicación y un solo paquete, los workspaces simples pueden ser suficientes. ¿Aún eligiendo un gestor de paquetes? Consulte nuestra comparación entre npm y pnpm.
Conclusión
Un buen monorepo es un entorno compartido donde las aplicaciones y los paquetes evolucionan bajo el mismo conjunto de herramientas. La capa tecnológica aquí es sencilla:
pnpm
+
Turborepo
+
TypeScript
Comience con dos aplicaciones:
apps/
├── web/
└── api/
Desarrolle hacia más servicios:
apps/
├── web/
├── admin/
├── api/
└── worker/
Sostenido por paquetes compartidos:
packages/
├── ui/
├── types/
├── database/
├── auth/
└── utils/
La ventaja radica en poder compartir código, tipos, configuraciones y flujos de trabajo, al tiempo que cada aplicación se mantiene organizada de forma independiente. A medida que la amplíe:
- declare las dependencias locales con
workspace:*para que las compilaciones se realicen en el orden correcto - enumere cada artefacto en
outputs, de lo contrario las restauraciones del caché lo pasarán por alto - conservar la configuración base en la raíz y ampliarla posteriormente
- agregue
packageManagery herramientas como ESLint antes de depender de los scripts de la raíz en CI
Referencias: la documentación de Turborepo, el repositorio de Turborepo, la documentación de pnpm y la documentación de Node.js.
Lecturas relacionadas
- Qué compartir entre aplicaciones NestJS, Next.js y Expo en un Turborepo — Configurar un espacio de trabajo de Turborepo para una API NestJS, un sitio Next.js y una aplicación Expo, y decidir qué tipos, esquemas y clientes deben incluirse en los paquetes compartidos.
- Dónde encaja Turborepo en un monorepo NestJS y cuándo saltárselo — Cómo Turborepo, los espacios de trabajo pnpm y NestJS dividen el trabajo en un monorepo de TypeScript: paquetes compartidos, gráficos de tareas, caché, riesgos de acoplamiento y cuándo saltárselo.