Menu

package.json: scripts, dependencias y versiones

Qué hay dentro del package.json: los campos que de verdad importan, cómo funcionan los scripts y cómo los rangos de semver deciden qué versiones instala npm.

El archivo manifiesto de un proyecto Node

Todo proyecto de Node.js tiene un package.json en la raíz. Es un simple archivo JSON que describe el proyecto: su nombre, su versión, de qué depende y qué comandos expone. Es justamente lo que lee npm cada vez que hace algo. Si lo borras, npm no tiene ni idea de qué va tu proyecto.

La forma más rápida de crearlo es con npm init:

npm init -y

La bandera -y salta las preguntas interactivas y acepta los valores por defecto. El resultado es algo así:

{
  "name": "my-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Ese es el esqueleto básico. La mayoría de esos campos no hacen gran cosa por sí solos: cobran sentido a medida que vas sumando dependencias y scripts.

Dependencies vs devDependencies

Hay dos campos que cargan con casi todo el peso: dependencies y devDependencies. Ambos son objetos que asocian nombres de paquetes con rangos de versiones.

index.js
Output
Click Run to see the output here.

La distinción importa por una razón: las dependencies son paquetes que tu código necesita para ejecutarse. Las devDependencies son paquetes que solo necesitas mientras desarrollas: test runners, linters, herramientas de build, verificadores de tipos. Cuando alguien instala tu paquete como dependencia suya, npm descarga tus dependencies y omite las devDependencies.

npm actualiza estos campos automáticamente. npm install express agrega una línea a dependencies. npm install --save-dev vitest agrega una a devDependencies. Rara vez tendrás que editarlos a mano.

Rangos de versiones en npm: ^, ~ y versión exacta

Esas cadenas de versión como ^4.19.0 no son versiones exactas, son rangos. npm sigue semver, que divide las versiones en MAJOR.MINOR.PATCH:

  • Un cambio en MAJOR rompe la compatibilidad hacia atrás.
  • Un cambio en MINOR añade funciones sin romper nada.
  • Un cambio en PATCH corrige bugs.

Los dos operadores que vas a ver por todos lados:

"express": "^4.19.0"   // >= 4.19.0 and < 5.0.0  (any 4.x.x at or above 4.19.0)
"express": "~4.19.0"   // >= 4.19.0 and < 4.20.0 (any 4.19.x at or above 4.19.0)
"express": "4.19.0"    // exactly 4.19.0

^ es el valor por defecto que npm usa al instalar un paquete. Confía en que las subidas de versión minor y patch mantendrán la compatibilidad. ~ es más conservador: solo permite actualizaciones de patch. Y una versión sin símbolo fija la versión exacta.

El detalle importante: "lo que acabo de instalar" y "lo que el rango permite" no son lo mismo. Si hoy instalas express@4.19.0 y un compañero clona tu proyecto dentro de un mes, ^4.19.0 podría resolverse a 4.19.5. Ahí es donde entra package-lock.json: deja registradas las versiones exactas que se resolvieron, para que todo el equipo termine con el mismo árbol de dependencias. Súbelo al repo.

Scripts en package.json: los atajos de tu proyecto

El campo scripts es donde defines atajos para los comandos que usas a menudo. Cualquier cosa que pongas ahí la puedes ejecutar con npm run <nombre>:

index.js
Output
Click Run to see the output here.

Algunas cosas que conviene saber sobre los scripts:

  • npm start, npm test y algunos otros nombres reservados funcionan sin la palabra run. Para el resto necesitas npm run <nombre>.
  • Los scripts se ejecutan en una shell que tiene node_modules/.bin en el PATH, así que puedes invocar directamente los binarios de los paquetes instalados. "test": "vitest" funciona aunque vitest no esté instalado globalmente.
  • Puedes encadenar scripts: "build": "npm run lint && npm run compile". Usa && cuando quieras decir "ejecuta en orden y corta si algo falla".
  • Los scripts pre<nombre> y post<nombre> se ejecutan de forma automática. Si defines prebuild, se ejecutará antes de build sin que tengas que configurar nada más.

Los scripts son la interfaz de comandos del proyecto. Un buen package.json hace que alguien que acaba de clonar el repo pueda lanzar npm install y luego npm run dev o npm test sin tener que rebuscar en una wiki.

Puntos de entrada: main, exports y type

Estos campos le indican a Node (y a los bundlers) cómo cargar tu paquete.

index.js
Output
Click Run to see the output here.
  • type define cómo se interpretan los archivos .js. Con "module" usas ESM (import / export). Si lo omites o pones "commonjs", se usa CommonJS (require). Tienes todos los detalles en el documento de CommonJS vs ESM.
  • main es el punto de entrada clásico: lo que resuelve require("my-lib"). Las herramientas más antiguas todavía lo tienen en cuenta.
  • exports es su reemplazo moderno y más estricto. Especifica exactamente qué archivos puede importar quien consume tu paquete y bajo qué subruta. Si un archivo no aparece aquí, la importación falla — y eso es una virtud, no un defecto. Tú decides cuál es la API pública.

Si solo estás montando una aplicación (y no publicando un paquete), lo único de esto que probablemente te importe es type.

Un package.json realista

Juntando todo, así se ve más o menos el package.json de una app pequeña de Node en la vida real:

index.js
Output
Click Run to see the output here.

Fíjate en engines.node. Es orientativo: npm lanza un warning (o un error si activas engine-strict) cuando la versión de Node del usuario no encaja. Es buena costumbre incluirlo en cualquier cosa que vayas a publicar.

Otros campos que conviene conocer

Hay varios campos más que te vas a encontrar por ahí:

  • private: true — evita que publiques el paquete en npm sin querer. Ponlo en cualquier proyecto que no esté pensado para publicarse.
  • license — un identificador SPDX tipo "MIT" o "ISC". Importa si el proyecto es público.
  • repository, bugs, homepage — se muestran en la ficha del paquete en el registro de npm.
  • bin — si tu paquete incluye una CLI, aquí es donde mapeas nombres de comandos a archivos de script. Tras la instalación, esos comandos quedan disponibles para ejecutarse.
  • workspaces — para monorepos; le dice a npm que trate ciertos subdirectorios como paquetes enlazados.

No necesitas todos. Necesitas los que tengan sentido para lo que estás haciendo.

Errores habituales

Un par de cosas con las que la gente tropieza una y otra vez:

  • Subir node_modules al repo. No lo hagas. Añádelo al .gitignore. Con package.json y package-lock.json cualquiera puede reconstruirlo con npm install.
  • No versionar package-lock.json. Sí hay que subirlo. Sin el lockfile, lo de "en mi máquina funciona" deja de ser un meme y se vuelve un problema real, porque los rangos de semver pueden resolverse a versiones distintas con el tiempo.
  • Meter dependencias de runtime en devDependencies. Localmente todo va bien porque las dev deps están instaladas, pero en producción te explota porque ahí se saltan. Si el código que despliegas lo usa, va en dependencies.
  • Editar versiones a mano sin reinstalar. Si cambias una versión en package.json, lanza npm install. Si no, node_modules y el lockfile se te quedan descuadrados.

Siguiente paso: el runtime de Node

package.json le dice a Node qué es tu proyecto. El runtime de Node decide cómo se ejecuta: la resolución de módulos, los módulos nativos, los globales, el event loop por debajo. De eso va la siguiente página.

Preguntas frecuentes

¿Para qué sirve el package.json?

Es el manifiesto de un proyecto Node.js. Ahí se guardan el nombre y la versión del proyecto, los paquetes de los que depende, los scripts que puedes lanzar con npm run y metadatos como el punto de entrada o el tipo de módulo. Cuando ejecutas npm install, npm lee este archivo para saber qué descargar.

¿Cuál es la diferencia entre dependencies y devDependencies?

En dependencies van los paquetes que tu código necesita en tiempo de ejecución, como express o react. En devDependencies van los que solo usas mientras desarrollas o compilas: test runners, bundlers, linters… Cuando alguien instala tu paquete como dependencia de su proyecto, npm se salta tus devDependencies.

¿Qué significan ^ y ~ en las versiones del package.json?

Son operadores de rango de semver. ^1.2.3 acepta cualquier versión 1.x.x igual o superior a 1.2.3 (mismo major). ~1.2.3 es más estricto: acepta 1.2.x igual o superior a 1.2.3 (mismo minor). Si pones 1.2.3 a secas, fijas la versión exacta. El package-lock.json guarda las versiones resueltas exactas para que las instalaciones sean reproducibles.

¿Cómo creo un archivo package.json?

Ejecuta npm init en un directorio vacío y responde a las preguntas, o usa npm init -y para aceptar los valores por defecto y generarlo al instante. También puedes escribirlo a mano: al final no deja de ser JSON. Los únicos campos realmente obligatorios son name y version.

Aprende a programar con Coddy

COMENZAR