Clonas otro proyecto, instalas sus dependencias y vuelves a descargar o materializar muchos paquetes que ya utilizas en otros repositorios. Después aparece un problema menos visible: un archivo importa una librería que nadie declaró en package.json, pero funciona porque otra dependencia la dejó accesible en node_modules.
pnpm propone una forma distinta de organizar esa instalación: reutiliza contenido entre proyectos y construye un árbol de dependencias que hace más explícito qué puede importar cada paquete. Eso puede resultar útil tanto en una aplicación pequeña como en un repositorio con varias aplicaciones y librerías compartidas.
Vamos a entender ese diseño y probarlo con una aplicación sencilla de Node.js y TypeScript. No necesitamos un monorepo para empezar, ni asumir que cambiar de gestor hará automáticamente más rápido cualquier proyecto.
Qué es pnpm y qué comparte con npm y Yarn
pnpm es un gestor de paquetes: resuelve versiones, obtiene dependencias, prepara su instalación y ejecuta scripts del proyecto. Puede consumir los paquetes del registro npm; no exige que sus autores publiquen una edición especial para pnpm.
Los tres gestores trabajan con package.json, pero sus archivos de bloqueo y sus estrategias de instalación no son intercambiables:
| Gestor | Organización habitual | Archivo de bloqueo |
|---|---|---|
| npm | node_modules, con dependencias elevadas a niveles superiores cuando es posible | package-lock.json |
| pnpm | Store compartido y enlaces; por defecto, instalación aislada mediante node_modules/.pnpm | pnpm-lock.yaml |
| Yarn moderno | Plug’n’Play por defecto; también admite los modos node-modules y pnpm | yarn.lock |
Elevar dependencias, o hoisting, permite colocar una dependencia en un directorio superior para que varios paquetes la encuentren. No significa que todas las versiones puedan fusionarse: pueden coexistir instalaciones cuando los requisitos difieren. La documentación de npm sobre carpetas describe su resolución y disposición.
Tampoco conviene hablar de «Yarn» como si solo existiera Yarn Classic. Los modos de instalación de Yarn incluyen Plug’n’Play, que utiliza un mecanismo de resolución sin el node_modules tradicional. Los tres gestores ofrecen workspaces; ese concepto no es exclusivo de pnpm.
El store: reutilizar archivos por su contenido
El content-addressable store es un almacén que identifica contenido mediante hashes. La idea importante es que la identidad del archivo depende de sus bytes, no únicamente del nombre y la versión del paquete.
Si dos versiones de una librería contienen muchos archivos idénticos, esos archivos pueden reutilizarse. Cuando varios proyectos necesitan el mismo contenido, no es necesario guardar una copia independiente completa por proyecto.
pnpm importa los archivos mediante enlaces duros o clonación copy-on-write cuando el sistema de archivos lo permite; puede recurrir a copias. Un enlace duro referencia los mismos datos del archivo; una clonación puede compartir bloques hasta que una escritura obliga a separarlos. No son lo mismo que un enlace simbólico, que apunta a otra ruta. La explicación oficial del almacenamiento distingue estos mecanismos.
Esto reduce duplicación en escenarios favorables, pero no demuestra por sí solo una cifra de ahorro ni un tiempo de instalación. Influyen la caché disponible, la red, el sistema de archivos, las dependencias y sus scripts. En este artículo no se comparan tiempos entre gestores.
Puedes consultar dónde está tu store:
pnpm store pathNo necesitas moverlo ni limpiarlo para seguir el tutorial. Si administras su contenido, consulta los comandos del store; eliminar paquetes reutilizables puede obligar a descargarlos en instalaciones posteriores.
Qué hay realmente dentro de node_modules
El store compartido y el virtual store del proyecto cumplen funciones diferentes. El primero conserva contenido reutilizable; el segundo organiza paquetes y relaciones para esa instalación.
Con la configuración aislada predeterminada, una dependencia directa como Zod aparece aproximadamente así:
node_modules/
├── zod -> .pnpm/zod@4.6.7/node_modules/zod
└── .pnpm/
└── zod@4.6.7/
└── node_modules/
└── zod/El enlace superior permite que nuestra aplicación importe zod. Las dependencias de cada paquete se conectan dentro de esta estructura según su propio grafo. Las combinaciones de peer dependencies pueden producir nombres más complejos: este esquema solo muestra el caso sencillo del laboratorio. La estructura de enlaces documentada por pnpm explica cómo encaja con la resolución de Node.js.
Una consecuencia útil es que importar una dependencia transitiva no declarada deja de ser algo en lo que puedas confiar. Si tu código importa una librería, declárala en el paquete que la utiliza. pnpm puede hacer hoisting interno por compatibilidad, de modo que tampoco debemos describir su aislamiento como una barrera de seguridad absoluta.
No edites archivos dentro de node_modules: son artefactos administrados por el gestor, no el lugar para mantener cambios propios.
Instalar pnpm y fijar su versión
La documentación vigente corresponde a pnpm 12. Para este tutorial utilizaremos 12.16.1, una versión concreta que permite repetir los comandos. Mantén Node.js en una versión LTS soportada y actualizada; Node 24 es una opción para el ejemplo.
Si ya tienes Node.js y npm, una vía sencilla es:
npm install -g pnpm@12.16.1
pnpm --versionEse comando instala una herramienta global; no migra tus proyectos. Si tu equipo administra las herramientas con otro mecanismo, utiliza ese mecanismo y evita superponer instalaciones en el PATH.
pnpm 12 se distribuye como ejecutable nativo: no requiere Node.js para ejecutarse una vez instalado. La aplicación del tutorial sí lo necesita. Los requisitos del instalador dependen de la vía elegida; revisa la guía de instalación si utilizas otra plataforma o una versión distinta.
Qué papel tiene Corepack actualmente
Corepack ayuda a seleccionar el gestor indicado por el proyecto. Se distribuyó con Node.js desde 14.19 hasta las versiones anteriores a 25; no supongas que viene incluido con cualquier Node moderno. Algunas distribuciones también lo empaquetan de otra manera.
Comprueba su presencia:
corepack --versionSi eliges Corepack como alternativa a la instalación directa, revisa primero sus requisitos de Node y posibles binarios en conflicto. Con una versión de Node compatible:
npm install -g corepack@latest
corepack enable pnpmDentro de un proyecto con package.json, puedes fijar la versión:
corepack use pnpm@12.16.1use modifica el manifiesto y ejecuta la instalación; revisa su diff. Corepack es opcional, no un requisito de pnpm. Su documentación oficial explica instalación y selección de versiones. También conviene comprobar sus requisitos actuales de Node: instalarlo en un runtime no soportado no se arregla ignorando un aviso de compatibilidad.
Proyecto práctico: un pedido validado con TypeScript
Crearemos una utilidad de terminal que recibe una cantidad y calcula el total de un pedido ficticio. Zod será una dependencia de ejecución; TypeScript y los tipos de Node serán herramientas de desarrollo.
Crea una carpeta vacía y entra en ella:
mkdir pnpm-lab
cd pnpm-lab
mkdir srcGuarda este package.json:
{
"name": "pnpm-lab",
"version": "1.0.0",
"private": true,
"type": "module",
"packageManager": "pnpm@12.16.1",
"scripts": {
"check": "tsc --noEmit",
"build": "tsc",
"start": "node dist/index.js"
}
}private evita publicar accidentalmente el paquete. type selecciona módulos ESM. packageManager registra la versión que debe utilizar el equipo y que herramientas como Corepack pueden respetar; configura también el entorno de CI para usar esa versión.
Instala las dependencias:
pnpm add --save-exact zod@4.6.7
pnpm add -D --save-exact typescript@5.9.3 @types/node@22.20.5Fijamos estas versiones para el ejercicio; no son una recomendación de permanecer indefinidamente en ellas. Los tipos de Node 22 cubren las APIs utilizadas aquí, aunque ejecutemos el programa con Node 24.
add guarda por defecto en dependencies; -D utiliza devDependencies. --save-exact evita introducir un rango como ^4.6.7. Puedes revisar estas opciones en pnpm add.
Crea tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"types": ["node"],
"skipLibCheck": true
},
"include": ["src/**/*.ts"]
}La configuración NodeNext de TypeScript modela el sistema de módulos de Node. Aquí compilamos explícitamente: ejecutar TypeScript y comprobar sus tipos son responsabilidades distintas.
Guarda el siguiente código en src/index.ts:
import { z } from 'zod';
const Pedido = z.object({
producto: z.string().trim().min(1),
cantidad: z.number().int().min(1).max(100),
precioCentavos: z.number().int().min(0).max(1_000_000),
});
const entrada: unknown = {
producto: 'Cuaderno',
cantidad: Number(process.argv[2] ?? '2'),
precioCentavos: 1500,
};
const resultado = Pedido.safeParse(entrada);
if (!resultado.success) {
console.error('Pedido inválido: la cantidad debe ser un entero entre 1 y 100.');
process.exitCode = 1;
} else {
const pedido = resultado.data;
const total = pedido.cantidad * pedido.precioCentavos;
console.log(`${pedido.cantidad} x ${pedido.producto}: ${total} centavos`);
}La entrada comienza como unknown y se valida antes de utilizarla. Si quieres profundizar en esa decisión, revisa por qué TypeScript no valida los datos en runtime y cómo ayuda Zod.
Ahora ejecuta:
pnpm run check
pnpm run build
pnpm start
pnpm start 3Las dos últimas órdenes producen, respectivamente:
2 x Cuaderno: 3000 centavos
3 x Cuaderno: 4500 centavosTambién puedes comprobar el rechazo:
pnpm start 0El programa muestra Pedido inválido: la cantidad debe ser un entero entre 1 y 100. y termina con código 1. Son resultados comprobados en el laboratorio. El gestor puede añadir sus propias líneas informativas alrededor de la salida.
Scripts, ejecutables y dependencias de producción
pnpm run build ejecuta el script definido en el manifiesto. Durante esa ejecución, los binarios locales están disponibles: tsc corresponde al compilador instalado en el proyecto, sin exigir una instalación global. pnpm run documenta ese comportamiento.
Para ejecutar directamente un binario local:
pnpm exec tsc --versionexec añade node_modules/.bin al entorno del comando. No equivale a descargar una herramienta arbitraria; para herramientas temporales existe pnpm dlx, que merece la misma revisión de procedencia que cualquier código descargado. Consulta pnpm exec.
En este ejemplo, el JavaScript compilado todavía importa Zod. Por eso Zod debe estar en dependencies. TypeScript se necesita al compilar y puede quedar en devDependencies.
Una instalación de producción con pnpm install --prod --frozen-lockfile omite las dependencias de desarrollo. No la hagas antes de compilar si necesitas tsc. En un despliegue con etapas separadas, la etapa de build instala las herramientas, genera dist y la etapa de ejecución recibe el resultado más las dependencias que necesita. Guardar algo en devDependencies no es una medida de aislamiento ni de protección de secretos.
El lockfile forma parte del proyecto
package.json expresa dependencias y rangos admitidos; pnpm-lock.yaml registra la resolución concreta y las relaciones necesarias para reproducirla. Versiona ambos. No guardes node_modules ni los archivos compilados de este ejercicio:
node_modules/
dist/
.envEn una clonación o en CI:
pnpm install --frozen-lockfile
pnpm run check
pnpm run build--frozen-lockfile impide regenerar silenciosamente el bloqueo y falla si no coincide con el manifiesto. Lo comprobamos cambiando temporalmente una versión de package.json: pnpm rechazó la instalación. La documentación de install detalla también el comportamiento en CI.
El lockfile mejora la repetibilidad, pero no congela el sistema operativo, Node, las herramientas nativas ni todos los efectos de scripts de instalación. Conserva una versión acordada de pnpm y un entorno coherente.
Si aparece un conflicto del lockfile al integrar ramas, resuelve primero la intención de las dependencias, regenera con la versión acordada y revisa el diff. No borres el archivo como solución automática a cualquier error de instalación. Las decisiones de integración también importan: aquí tienes una comparación de merge, rebase y cherry-pick.
Comandos para el trabajo diario
| Objetivo | Comando |
|---|---|
| Instalar lo declarado | pnpm install |
| Añadir una dependencia | pnpm add nombre-del-paquete |
| Añadir una herramienta de desarrollo | pnpm add -D nombre-del-paquete |
| Eliminar una dependencia | pnpm remove nombre-del-paquete |
| Ver dependencias directas | pnpm list --depth 0 |
| Entender por qué está instalada Zod | pnpm why zod |
| Consultar versiones pendientes | pnpm outdated |
| Actualizar Zod dentro del rango declarado | pnpm update zod |
En el laboratorio Zod está fijado exactamente, de modo que pnpm update zod no lo mueve a otra versión. Con un rango, el comando puede seleccionar una versión compatible y actualizar los archivos correspondientes. pnpm update explica qué modifica.
Para solicitar explícitamente una versión nueva y revisada:
pnpm add --save-exact zod@VERSION_REVISADA
pnpm run check
pnpm run build
pnpm start 3VERSION_REVISADA es un marcador que debes sustituir, no una versión publicable ni un comando listo para copiar. pnpm update --latest puede saltar fuera de los rangos existentes: no lo conviertas en una corrección automática, porque una versión mayor puede introducir cambios incompatibles.
Outdated informa de versiones posteriores; no diagnostica por sí solo una vulnerabilidad. Para ese análisis hay otro proceso, explicado en el artículo sobre SCA y dependencias vulnerables en Node.js.
Workspaces: varios paquetes en un repositorio
Un monorepo agrupa proyectos en un repositorio. Un workspace permite que el gestor los identifique, instale sus dependencias y enlace paquetes locales. No resuelve automáticamente la arquitectura, los despliegues o el reparto de responsabilidades del equipo.
Imagina esta estructura, independiente del ejercicio anterior:
mi-repositorio/
├── package.json
├── pnpm-workspace.yaml
├── apps/
│ └── api/package.json
└── packages/
└── utilidades/package.jsonEn la raíz, declara los paquetes en pnpm-workspace.yaml:
packages:
- 'apps/*'
- 'packages/*'Cada paquete tiene su manifiesto y un nombre único. Si los nombres son @demo/api y @demo/utilidades, puedes declarar una dependencia local desde la raíz:
pnpm --filter @demo/api add '@demo/utilidades@workspace:*'El protocolo workspace: exige resolver el paquete dentro del workspace; evita sustituirlo silenciosamente por otro del registro. No convierte el código fuente TypeScript de una librería en JavaScript por sí solo: sus exports y su compilación siguen siendo responsabilidad del paquete. La guía de workspaces explica el protocolo y el comportamiento al empaquetar.
Para ejecutar el script check de los paquetes:
pnpm -r run check
pnpm --filter @demo/api run checkLa segunda orden selecciona solo la API. Los filtros de pnpm permiten trabajar con nombres, rutas o relaciones entre paquetes. Un workspace aporta coordinación; no significa que todas las tareas deban ejecutarse siempre sobre todo el repositorio.
Migrar desde npm sin mezclar instalaciones
Haz la migración en una rama y parte de una instalación funcional. Conserva el lockfile de npm mientras lo importas:
git switch -c chore/migrar-pnpm
pnpm importImport genera pnpm-lock.yaml a partir de archivos como package-lock.json. No es una promesa de conversión idéntica en cualquier situación: revisa las versiones resueltas y cualquier diferencia con los manifiestos.
Después, elimina únicamente el node_modules generado del proyecto —comprueba la carpeta antes— e instala de nuevo con pnpm:
pnpm install --frozen-lockfileEjecuta los checks, tests y build que realmente existan en ese repositorio. Si todo funciona, retira package-lock.json, registra packageManager y actualiza los comandos de CI, Docker y documentación. En un monorepo, revisa también pnpm-workspace.yaml y las dependencias entre paquetes.
No mantengas dos lockfiles activos por inercia ni alternes gestores sobre la misma instalación. La migración debe poder revisarse y revertirse como un cambio acotado, sin mezclarla innecesariamente con actualizaciones masivas.
Compatibilidad: qué revisar cuando algo falla
No encuentra un módulo. Comprueba si tu paquete lo declara o dependía accidentalmente del hoisting de otro gestor. La corrección suele ser declarar la dependencia donde se utiliza.
Una herramienta no maneja bien los enlaces simbólicos. Confirma primero su soporte. pnpm permite configurar nodeLinker: hoisted, pero cambia la disposición de la instalación; úsalo para un problema concreto, no como primera respuesta a cualquier fallo. Las opciones de node_modules documentan alternativas y límites.
Un paquete necesita un script de instalación. Las versiones actuales controlan qué dependencias pueden ejecutar esos scripts. Revisa qué intenta ejecutar el paquete y utiliza pnpm approve-builds si corresponde. La decisión queda en la configuración del proyecto; no autorices todos los scripts solo para silenciar un aviso. Consulta approve-builds para la versión usada.
La configuración antigua no parece aplicarse. En las versiones actuales, buena parte de la configuración reside en pnpm-workspace.yaml; no copies indiscriminadamente opciones de tutoriales antiguos de .npmrc. Revisa las fuentes de configuración vigentes. Los datos de autenticación requieren tratamiento separado y no deben terminar en Git.
El lockfile no coincide o usa otro formato. Compara la versión del gestor local y de CI antes de regenerarlo. Si el manifiesto cambió intencionadamente, actualiza el lockfile en desarrollo y revisa ambos archivos; no desactives el modo congelado en CI para ocultar el desajuste.
Cuándo merece la pena adoptar pnpm
pnpm resulta especialmente interesante cuando trabajas en varios proyectos con dependencias compartidas, necesitas workspaces o quieres detectar dependencias que tu código utiliza sin declararlas. Su modelo permite discutir el árbol instalado con más precisión que simplemente contar carpetas.
npm puede ser suficiente cuando el proyecto es sencillo, el equipo ya tiene un flujo reproducible y no existe un problema concreto que justifique la migración. Adoptar pnpm también implica acordar versiones, adaptar automatizaciones y comprobar compatibilidad.
Una prueba pequeña como la de este artículo permite evaluar lo importante: instalación limpia, scripts que funcionan, lockfile revisable y dependencias declaradas correctamente. Decide con esos resultados y con mediciones de tu proyecto si necesitas comparar rendimiento, sin convertir la elección del gestor en una regla universal.
