Hexadevs
Volver al blog

Monorepos con pnpm: cuando dejan de doler y empiezan a servir

Por Equipo Hexadevs · 28 ago 2026 · 3 min de lectura

Un monorepo mal configurado es una de las fuentes más comunes de fricción en equipos chicos y medianos. Un monorepo bien configurado, en cambio, te devuelve atomicidad en los cambios, código compartido sin publicar paquetes y CI que corre solo lo necesario.

Por qué pnpm y no npm workspaces

npm puede hacer workspaces desde la versión 7, pero pnpm resuelve tres problemas que npm y yarn no:

  1. No duplica dependencias en disco. Un proyecto con 15 paquetes no instala 15 copias de React; usa symlinks a un store global. Según los benchmarks del equipo de pnpm, instalación hasta 3x más rápida, y un árbol de node_modules plano.
  2. --filter para ejecutar comandos selectivamente. Correr tests solo en el paquete afectado y sus dependientes.
  3. Strict mode por defecto, que evita que un paquete acceda a dependencias que no declaró explícitamente. Esto previene el clásico bug de “funciona en este paquete pero revienta cuando lo importás desde otro”.

Estructura mínima viable

El típico monorepo con pnpm y workspaces:

  • hexadevs/package.json — workspace root
  • hexadevs/pnpm-workspace.yaml
  • hexadevs/packages/ui/ — componentes compartidos
  • hexadevs/packages/eslint-config/ — configs compartidas
  • hexadevs/packages/tsconfig/ — tsconfigs base
  • hexadevs/apps/web/ — front (en nuestro caso Astro)
  • hexadevs/apps/api/ — backend (Hono server)
# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"
// package.json (root)
{
  "name": "hexadevs",
  "private": true,
  "scripts": {
    "dev": "pnpm --filter \"./apps/*\" run dev",
    "build": "pnpm -r run build",
    "test": "pnpm -r --parallel run test",
    "lint": "pnpm -r run lint"
  }
}

Tasks recursivas y filtrado, las dos joyitas

# Un paquete y sus dependencias
pnpm --filter @hexadevs/ui... run build

# Solo los paquetes que dependen de ui (sin incluir ui)
pnpm --filter ...^@hexadevs/ui run test

# Por nombre con glob
pnpm --filter "./apps/*" run dev

Con estas tres formas de filtrado resolvés casi todos los casos. El resto se cubre con --filter-prod, --parallel y --topological.

Dependencias entre paquetes: el detalle que rompe todo

Acá está el gotcha más común. Para que apps/web use packages/ui correctamente:

// apps/web/package.json
{
  "dependencies": {
    "@hexadevs/ui": "workspace:*"
  }
}

workspace:* es la sintaxis de pnpm: apunta a la versión local del paquete y, al publicar, pnpm la reemplaza por la versión real. Nunca uses ^1.0.0 ni link: para esto: rompe la actualización de versiones entre paquetes.

La capa que falta: caché de tareas con Turborepo

pnpm ordena y filtra, pero sigue siendo serial y ciego: pnpm -r run build vuelve a construir los 6 paquetes aunque solo cambió uno. Esa capa la resuelve Turborepo, que corre tus scripts entendiendo el grafo de dependencias, en paralelo y con caché por contenido:

// turbo.json
{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    }
  }
}
pnpm exec turbo run build

dependsOn: ["^build"] construye primero las dependencias, y outputs le dice a Turborepo qué guardar en su caché. Si el hash del código fuente y de sus dependencias no cambió, el build se restaura de ahí (en la terminal lo vas a ver como cache hit, replaying logs). En CI el salto es aún mayor con el remote cache: todos los runners comparten la caché, y el build que tu compañero ya hizo no vuelve a computarse.

En nuestro monorepo conviven las dos herramientas sin pelearse: pnpm maneja dependencias (instalar, filtrar, vincular workspaces) y Turborepo maneja tareas (build, check, test) en local y en CI.

Si tu equipo tiene más de 3 desarrolladores y más de un deploy, un monorepo con pnpm deja de ser opcional. Es la diferencia entre coordinar releases y hacer un commit atómico que toca todos los paquetes afectados a la vez.

Cuando empieces, mantené el monorepo chiquito: 3 a 5 paquetes. Sumá uno nuevo solo cuando hay un caso claro de código compartido. Los monorepos grandes sin justificación son la forma más rápida de arruinar la productividad de un equipo.