Las cuatro listas de dependencias
dependencies, devDependencies, peerDependencies y optionalDependencies: qué instala cada una, quién las ve al consumir tu paquete, y el criterio exacto para decidir en cuál cae cada dependencia sin envenenar el arbol de nadie.
Un package.json no declara sus dependencias en una sola lista, sino en cuatro, y la diferencia entre ellas no es cosmética: gobierna qué acaba en el node_modules de quien te instala, qué se comparte como singleton con el anfitrión y qué puede faltar sin que nada se rompa. Colocar una dependencia en el campo equivocado es una de las causas más comunes de árboles hinchados, de plugins duplicados que se pisan y de builds que fallan solo en producción. Elegir bien es un acto de diseño, no de trámite.
- Distinguir con precisión qué instala cada uno de los cuatro campos y cuándo se activa.
- Separar lo que necesita el runtime del consumidor de lo que solo necesitas tú al desarrollar.
- Entender
peerDependenciescomo un contrato de compatibilidad, no como una simple dependencia. - Aplicar un árbol de decisión reproducible para clasificar cualquier dependencia nueva.
Las cuatro listas y quién las instala
La distinción cardinal es esta: cuando alguien instala tu paquete como dependencia, npm o pnpm arrastra tus dependencies de forma transitiva, pero ignora tus devDependencies. Ese único hecho ya reparte la mayor parte del trabajo. Las otras dos listas son casos especiales: peerDependencies expresa una expectativa sobre el entorno anfitrión, y optionalDependencies tolera el fallo de instalación sin abortar.
flowchart TD
Q0[Nueva dependencia] --> Q1{la usa el consumidor en runtime}
Q1 -->|no, solo al desarrollar| DEV[devDependencies]
Q1 -->|si| Q2{el anfitrion deberia proveerla como singleton}
Q2 -->|si| PEER[peerDependencies]
Q2 -->|no| Q3{el paquete funciona si su instalacion falla}
Q3 -->|si| OPT[optionalDependencies]
Q3 -->|no| DEP[dependencies]En el viejo node_modules plano de npm, todas las transitivas quedaban izadas a la raíz, así que tu código podía importar un paquete que no habías declarado —una dependencia de una dependencia— y funcionaba de milagro. Eso es una dependencia fantasma: aguanta hasta que el intermediario deja de incluirla y tu build se rompe sin que hayas tocado nada tuyo. pnpm lo previene con un node_modules no plano, enlazado desde un almacén global mediante symlinks, donde solo ves lo que declaraste. Es estricto a propósito: te obliga a nombrar cada dependencia que usas, y esa disciplina es justo lo que hace reproducible el árbol.
dependencies
Lo que tu código importa y ejecuta en producción. Viaja con el paquete a cada consumidor.
devDependencies
Andamiaje local: compilador, bundler, tests, linters, tipos. Desaparece para quien te instala.
peerDependencies
Un contrato: el anfitrión debe proveerlo como singleton. La clave de los plugins.
optionalDependencies
Se intenta instalar; si falla, no aborta. Binarios nativos por plataforma, con guardas en el código.
Cuando un paquete aparece en tu árbol y no sabes por qué, pnpm why <paquete> traza la cadena completa de quién lo pide, y con qué rango, hasta la raíz. Es la herramienta que convierte un node_modules opaco en un grafo legible: imprescindible para auditar por qué tienes tres versiones de la misma librería o quién arrastra esa dependencia que creías haber eliminado.
dependencies frente a devDependencies
En dependencies va todo lo que tu código importa y ejecuta en tiempo de ejecución: el framework HTTP, la librería de validación, el cliente de base de datos. Si al eliminar el paquete tu código lanza un Cannot find module en producción, pertenece aquí. Estas dependencias viajan con tu paquete cuando otro lo instala, así que cada entrada aquí es un coste que impones a todos tus consumidores.
En devDependencies va el andamiaje que solo existe en tu propio repositorio: el compilador de TypeScript, el bundler, el framework de tests, los linters, los tipos @types/*. Se instalan cuando ejecutas pnpm install dentro del proyecto, pero desaparecen para quien consume el paquete publicado. La prueba mental es directa:
- ¿Lo importa el código que se ejecuta en producción? →
dependencies. - ¿Solo aparece en scripts de build, tests o configuración local? →
devDependencies. - ¿Es una herramienta de línea de comandos que usas, no que ejecuta tu librería? →
devDependencies.
{
"dependencies": {
"zod": "^4.0.0"
},
"devDependencies": {
"typescript": "^5.9.0",
"vitest": "^3.2.0",
"@types/node": "^22.0.0"
}
}
El error clásico va en la dirección menos obvia. Declarar en devDependencies un paquete que tu código importa en runtime no falla nunca en tu máquina —ahí está todo instalado— y estalla solo cuando un tercero te consume en un entorno que no instala las dev. Por eso la pregunta correcta no es ¿lo tengo yo instalado?, sino ¿lo tendrá quien me use?.
En CI, tus devDependencies sí se instalan —son las que compilan, testean y empaquetan—, así que la frontera con dependencies no encoge tu propio pipeline; lo que encoge es el peso que impones a terceros cuando publicas. Por eso esta disciplina importa mucho más en una librería que en una aplicación de producto.
Una aplicación final —no una librería— es el caso donde la frontera se relaja: como nadie te instala como dependencia, la distinción importa menos para la corrección y más para la higiene y la velocidad de instalación en CI.
peerDependencies: el contrato con el anfitrión
peerDependencies no dice necesito instalar esto, sino necesito que quien me use ya tenga esto, y en esta versión. Es el mecanismo con el que un plugin de ESLint declara que necesita ESLint, o un componente de React declara que necesita React, sin traerse su propia copia. La razón es la unicidad: si un plugin instalara su propia React, habría dos copias en memoria, los hooks se romperían y el estado no se compartiría. El peer garantiza que exista un único ejemplar compartido, el del anfitrión.
- Úsalo para librerías que extienden o se enchufan a un ecosistema con estado o identidad únicos.
- Combínalo con
peerDependenciesMetapara marcar un peer como opcional cuando la integración es prescindible. - En 2026, pnpm resuelve los peers automáticamente por defecto (
auto-install-peers), pero mantiene un aislamiento estricto que hace visible cualquier peer no satisfecho en lugar de ocultarlo.
{
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": { "optional": true }
}
}
Elegir el rango de un peer es un ejercicio de equilibrio: demasiado estrecho —react: 18.2.0— y excluyes a consumidores legítimos; demasiado amplio —react: *— y prometes una compatibilidad que no puedes sostener. La convención sana es un rango de major abierto por arriba, >=18, que declara la versión mínima soportada y confía en el semver del anfitrión para el resto.
optionalDependencies y los binarios por plataforma
Una entrada en optionalDependencies se intenta instalar, pero si falla —porque no hay binario para esa plataforma, por ejemplo— la instalación continúa sin error. El patrón canónico son los paquetes de binarios nativos específicos de sistema operativo o arquitectura: fsevents solo en macOS, o los paquetes por plataforma que publican esbuild, @rollup/rolldown y @swc/core. El precio a pagar es que tu código debe proteger su uso con un try/catch o una comprobación de existencia, porque no puedes asumir que estará. Si una misma dependencia aparece en dependencies y en optionalDependencies, gana la opcional. Existe además bundledDependencies, una lista de paquetes que se empaquetan dentro de tu propio tarball al publicar, útil en escenarios de distribución cerrada pero excepcional en el trabajo cotidiano.
Casos reales donde optionalDependencies es la herramienta correcta:
- Vigilancia de ficheros:
fseventssolo tiene sentido en macOS y sobra en el resto. - Binarios de compilación por plataforma que publican
esbuild,@rollup/rolldowno@swc/core. - Aceleradores nativos opcionales que degradan a una implementación en JavaScript si no están disponibles.
La regla de oro con las opcionales es que el camino feliz nunca debe depender de ellas: son una mejora si están y una ausencia tolerable si no. En cuanto tu código deje de funcionar sin una de ellas, ha dejado de ser opcional y debe mudarse a dependencies.
A veces el problema no es una dependencia tuya, sino una transitiva —una dependencia de una dependencia— con un fallo de seguridad que su autor aún no ha parcheado. Para eso existe pnpm.overrides, cuyo equivalente en npm es overrides y en yarn, resolutions: reescribe la versión resuelta de un paquete en cualquier punto del árbol, saltándose el rango que pidió el intermediario.
{
"pnpm": {
"overrides": {
"cross-spawn@<7.0.5": ">=7.0.5"
}
}
}Es un bisturí, no un martillo: cada override es deuda técnica que conviene revisar y retirar cuando la cadena upstream se corrige, porque estás contradiciendo a mano lo que los autores declararon compatible.
El coste de clasificar mal una dependencia casi nunca se paga en el momento; se paga después, y lo paga otro. Poner en dependencies algo que solo usas al desarrollar —un framework de tests, un generador de tipos— infla el árbol de cada consumidor con megabytes que nunca ejecutará, ralentiza cada instalación en cada CI del mundo que dependa de ti y agranda la superficie de ataque de la cadena de suministro. El error inverso es más traicionero: poner en devDependencies algo que tu código importa en runtime funciona perfectamente en tu máquina, porque ahí todo está instalado, y estalla solo cuando un tercero te consume y el módulo no aparece. Y confundir una dependency con un peer en una librería de plugins produce el bug más difícil de diagnosticar de todos: dos copias del framework coexistiendo, hooks que fallan sin motivo aparente, estado que no se propaga. Por eso los cuatro campos no son cajones administrativos, sino una declaración precisa de la topología de tu paquete: qué es tuyo, qué es del anfitrión, qué es prescindible y qué es solo tuyo mientras construyes. Diséñala con la misma seriedad que diseñas una API pública, porque eso es exactamente lo que es.
- Abre el
package.jsonde una librería popular que sea un plugin (por ejemplo, un plugin de ESLint o de Vite) y estudia por qué su framework anfitrión está enpeerDependenciesy no endependencies. - En un proyecto tuyo, recorre las
dependenciesuna a una y pregúntate: ¿la importa código que corre en producción? Mueve adevDependenciescada intrusa. - Localiza en algún
node_modulesun paquete conoptionalDependenciespor plataforma y observa cómo publica un binario distinto por sistema operativo. - Escribe en una frase la regla que usarás en adelante para decidir entre
dependenciesypeerDependenciesal publicar una librería.