Como funciona node modules
Cuando un proyecto Node.js declara una dependencia en su package.json, el nombre de esa librería aparece en el código, pero ninguno de sus archivos está dentro del repositorio. Todo lo que agregaste vive en una carpeta llamada node_modules, y esa carpeta es, sin exagerar, la parte del proyecto que menos se revisa y la que más puede cambiar el comportamiento de todo lo demás.
Este texto no trata de cómo funciona internamente npm al descargar un paquete. Trata de lo que ocurre después: qué significa para tu aplicación que ese código esté ahí, qué queda atado cuando lo incorporas y qué tan dependiente se vuelve el proyecto de una decisión que tomó otra persona.
La carpeta que no se versiona
node_modules es un artefacto generado. No se guarda en el control de versiones y no tiene sentido editarlo a mano, porque cualquier instalación puede regenerarlo por completo.
Sin embargo, no es opcional. Si borras la carpeta, la aplicación deja de funcionar aunque el código fuente siga intacto. Eso significa que la carpeta contiene algo que tu proyecto necesita en tiempo de ejecución y que, al mismo tiempo, no es código tuyo.
mi-proyecto/
├── package.json declara qué necesitas
├── package-lock.json declara exactamente qué versión
├── src/ tu código
└── node_modules/ código de terceros, regenerable
La consecuencia es incómoda: el código real que se ejecuta en producción no está en tu repositorio. Una parte importante del sistema vive fuera del control de versiones, escrita por desconocidos, con licencia propia y con su propio ritmo de actualización.
node_modules es una tabla de resolución
La carpeta no es un almacén plano donde se busca "el paquete". Es el mecanismo que usa Node.js para resolver cada require o cada import.
Cuando Node encuentra require("express"), sube desde el archivo actual hacia la raíz del proyecto buscando una carpeta node_modules que contenga ese nombre. El primer hallazgo gana.
// src/pedidos/rutas.js
const express = require("express");
La búsqueda empieza en src/pedidos/node_modules, sigue por src/node_modules y termina en el node_modules de la raíz. Esa prioridad es la que permite que una versión se pise con otra cuando hace falta.
Cuando dos paquetes requieren versiones incompatibles, npm no reescribe la primera: crea una copia anidada.
node_modules/
├── express/
│ └── node_modules/
│ └── body-parser/ la versión que express necesita
└── body-parser/ la versión que pidió tu proyecto
El resultado es que la estructura de archivos es la semántica del proyecto. Dos instalaciones con el mismo package.json pueden terminar con árboles distintos y, por lo tanto, con comportamientos distintos si las versiones no están fijadas.
Qué pasa el día que incorporas una librería
El momento en que escribes el import es el momento en que tu proyecto empieza a depender de otro. A partir de ahí hay tres cosas que quedan conectadas.
La API pública pasa a ser parte de tu código. No dependes de su implementación, dependes de su interfaz: los nombres de las funciones, la forma de los objetos, el orden de los argumentos, los nombres de las opciones. Esa superficie se vuelve parte de tu código igual que una variable local.
const router = express.Router();
router.get("/pedidos/:id", async (req, res) => {
const pedido = await Pedido.findById(req.params.id);
res.json(pedido);
});
Aquí express.Router, get y findById no son un detalle menor: son el contrato que tu código asume. Si esa API cambia, tu código deja de funcionar, y si deja de funcionar, no hay forma de que un ajuste local lo resuelva.
Las dependencias transitivas también entran. Una librería casi nunca está sola. Sus propias dependencias se instalan en tu proyecto, y ahí aparece el problema de verdad: ya no sabes con certeza qué código se está ejecutando.
npm ls --all
Ese comando muestra el árbol real. Es habitual descubrir que una dependencia pequeña arrastra cientos de paquetes, y que ninguno de ellos es código que tu equipo haya revisado.
Puede ejecutar código al importarse. Depender de una librería no es solo depender de sus funciones. Una parte de los paquetes ejecuta código en el momento en que se importa: registra hooks, modifica prototipos, define globales o instrumenta librerías que ya estaban cargadas.
// Al escribir esto, el paquete puede ejecutarse antes de tu primera línea
import "una-libreria-que-configura-todo";
Ese comportamiento es legítimo, pero es invisible para quien lee tu código. Si algo raro ocurre en producción, el culpable puede estar en un import que ni siquiera parece importar nada.
De cuánto depende realmente tu proyecto
La dependencia técnica y la dependencia real casi nunca coinciden. Una librería puede estar en el package.json y aun así ser reemplazable en una tarde, o puede estar en una sola línea y ser imposible de quitar.
El costo de salida se puede estimar con preguntas concretas:
- ¿Cuántos archivos de tu proyecto la usan?
- ¿Su API aparece en tu lógica de negocio o solo en la capa de infraestructura?
- ¿Usas algo que el paquete no documenta, es decir, internos o rutas profundas?
- ¿Entiendes qué hace internamente cuando falla?
Si la respuesta es que la API aparece en la lógica de negocio, el acoplamiento es alto aunque el import aparezca una sola vez.
La prueba más honesta es imaginar el reemplazo: ¿qué costaría escribir esta parte con la API estándar del lenguaje, o con un par decientas líneas propias? Si la respuesta es "seis horas", la dependencia es razonable. Si la respuesta es "no tengo idea porque nunca vi qué hace por dentro", estás en un lugar frágil.
Lock-in no es lo mismo que usar una dependencia
Vale la pena distinguir dos cosas que a menudo se confunden.
Usar una dependencia es casi inevitable. Ninguna aplicación decente reimplementa el parseo de fechas, el cliente HTTP ni la criptografía; hacerlo es una forma de crear errores que otras personas ya resolvieron y probaron.
El lock-in aparece cuando el costo de cambiar de proveedor supera al de quedarse. Ocurre siempre en los mismos puntos:
- Cuando dependes de la API completa de un producto en lugar de un estándar abierto.
- Cuando la librería se filtra al dominio y sus conceptos quedan en tu lógica de negocio.
- Cuando usas una característica experimental o no documentada.
- Cuando la librería es el punto de entrada de tu build, tu servidor o tu despliegue.
- Cuando nadie del equipo sabe qué parte del comportamiento viene del paquete.
La diferencia es que una dependencia bien elegida se puede reemplazar; un lock-in también se puede quitar, pero cuesta un proyecto completo.
package-lock.json: reproducibilidad y confianza
El lock file guarda la versión exacta de cada paquete, incluidas las transitivas. Sin él, dos instalaciones del mismo package.json pueden resolver versiones distintas, y una actualización menor puede cambiar el comportamiento sin que nadie haya tocado una sola línea de código.
Esa reproducibilidad tiene un costo que se olvida con frecuencia: el lock file convierte a tu proyecto en un canal de confianza hacia cientos de autores y hacia los servidores de registro que publican sus paquetes.
El caso de event-stream es la referencia clásica. Un paquete extremadamente popular, sin mantenimiento visible, fue comprometido por su propio mantenedor, que agregó código capaz de robar credenciales de criptomonedas a los proyectos que lo tenían instalado. Nadie había escrito una sola línea nueva: bastó con publicar una versión nueva y esperar a que los pipelines hicieran su trabajo.
Ese episodio resume dos riesgos distintos que conviene no mezclar. Uno es confianza en el código: qué hace el paquete que instalaste. El otro es confianza en el canal: quién publicó esa versión y si el artefacto descargado es realmente el que está en el registro. Las sumas de verificación, los campos de integridad y los registros alternativos existen para acotar el segundo.
{
"dependencies": {
"left-pad": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz",
"integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA=="
}
}
}
Ese campo integrity no es decorativo: es la forma de detectar que el archivo que se descargó no es el que se esperaba.
Cómo reducir el lock-in
Ninguna de estas medidas elimina la dependencia, pero todas reducen el costo de salir.
- Encierra la librería en un adaptador. Escribe un módulo propio que exponga la funcionalidad que tu aplicación necesita y que sea el único lugar del proyecto que importa el paquete. Si algún día hay que reemplazarlo, el cambio se concentra en ese archivo.
// infra/logging.js (único punto que conoce el paquete)
import { createLogger } from "una-libreria-externa";
export const logger = createLogger({ nivel: "info" });
- Mantén las importaciones en la capa de infraestructura. Si el nombre del paquete aparece en un componente de interfaz o en una regla de negocio, el acoplamiento ya se filtró. La regla práctica es sencilla: los detalles de terceros no deberían viajar hacia el dominio.
- Fija las versiones y audita las actualizaciones. Un lock file revisado en cada actualización evita dos cosas: que entre una versión mayor sin revisión y que se acumulen vulnerabilidades conocidas por no estar al pendiente de los avisos.
npm outdated
npm audit
npm ls --depth=0
- Prefiere pocas dependencias grandes antes que muchas pequeñas. Cada dependencia directa es una frontera que puede cambiar sin aviso. Antes de sumar un paquete de treinta líneas, conviene preguntarse si el problema se resuelve con la biblioteca estándar.
- Observa las señales de mantenimiento. Fecha del último lanzamiento, cantidad de mantenedores, velocidad de respuesta a los issues y cambios en la licencia. Un proyecto sin nadie detrás es un lock-in que todavía no se ha manifesto, es decir, un riesgo que se paga solo.
- Revisa las dependencias transitivas cuando la directa se vuelve crítica. Si tu aplicación depende de una librería que a su vez depende de cuarenta paquetes, vale la pena saber quiénes mantienen esos cuarenta.
La pregunta que decide
Antes de incorporar cualquier paquete, la pregunta útil no es "cuántas líneas de código tiene", sino ¿el código de esta librería es un detalle de la aplicación o es el corazón de la funcionalidad?
Si es un detalle (parsear una fecha, generar un identificador, hablar con un servicio), conviene usarla y dejar de romanticar la dependencia. Si es el corazón, la decisión deja de ser técnica y pasa a ser estratégica: estás atando el proyecto a ese proveedor, a su ritmo de cambios y a su disponibilidad.
La idea importante
node_modules no es solo una carpeta que se regenera. Es el lugar donde vive una parte del sistema que tu equipo no controla.
Cuando incorporas una librería no estás agregando una función: estás agregando un contrato, un ritmo de cambios y un punto de falla. Entender eso no significa usar menos dependencias, significa elegirlas a conciencia, mantener la superficie de acoplamiento pequeña y saber siempre cuál sería el costo de irse.
© Arturo Juárez Monroy