Every Node.js project (and the vast majority of modern front-end projects) has a package.json file at its root. It’s the project’s “ID document”: it describes what the project is, which dependencies it needs, and how to run common tasks (run, test, build). This post explains the main fields and what each one does.
A basic package.json
// package.json
{
"name": "my-project",
"version": "1.0.0",
"description": "Brief project description",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "jest"
},
"dependencies": {
"express": "^4.19.0"
},
"devDependencies": {
"jest": "^29.7.0"
}
}
Identification fields
name: the project/package’s name. If you plan to publish to npm, it needs to follow specific rules (lowercase, no spaces, no special characters besides hyphens).
version: follows semantic versioning: MAJOR.MINOR.PATCH. Bump the patch number for bug fixes, minor for backward-compatible new features, and major for breaking changes.
description and keywords: metadata used mainly if the package gets published to npm, helping people find it in searches.
main: indicates which file is the package’s entry point when someone does require('your-package') or imports it.
Dependencies vs devDependencies
This is probably the most important distinction in the file.
// package.json
{
"dependencies": {
"express": "^4.19.0"
},
"devDependencies": {
"jest": "^29.7.0",
"eslint": "^9.0.0"
}
}
dependencies: packages needed to run the application in production (e.g., a web framework like Express).
devDependencies: packages needed only during development (tests, linters, build tools), which don’t need to ship to the production environment.
When you run npm install --production (or the equivalent in a production CI/CD pipeline), only dependencies get installed, keeping the production environment leaner.
Understanding version symbols
// package.json
{
"dependencies": {
"express": "^4.19.0",
"lodash": "~4.17.21",
"chalk": "5.3.0"
}
}
^ (caret): allows minor and patch version updates, but not major. ^4.19.0 accepts any 4.x.x version, but not 5.0.0.
~ (tilde): allows only patch updates. ~4.17.21 accepts 4.17.x, but not 4.18.0.
No symbol: locks to that exact version, with no automatic updates.
This flexibility matters because, when running npm install, npm follows these rules to decide which versions to install, within what’s available at the time.
The scripts field
// package.json
{
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest",
"build": "webpack --mode production",
"lint": "eslint ."
}
}
Each key inside scripts becomes a command you can run via npm run <name> (except for start and test, which can be run directly as npm start and npm test, without run). This standardizes common project tasks, so no developer has to remember the exact command for running tests or building the project.
package.json vs package-lock.json
A common point of confusion for beginners: package.json declares which dependencies the project needs and which versions are acceptable (using the ^ and ~ symbols). package-lock.json (generated automatically), on the other hand, locks the exact version of every installed dependency, including transitive dependencies (your dependencies’ own dependencies).
This guarantees that, when running npm install on another machine, everyone installs the exact same versions, avoiding the classic “works on my machine” problem. package-lock.json should always be committed to version control, alongside package.json.
Other common fields
// package.json
{
"engines": {
"node": ">=18.0.0"
},
"type": "module",
"private": true
}
engines: specifies which Node (or npm) versions the project supports. Doesn’t block installation by default, but serves as documentation and a warning.
type: defines whether the project uses ES modules (import/export) or CommonJS (require) by default. "module" enables ES modules.
private: when true, prevents the package from being accidentally published to npm (useful for internal projects that should never be published publicly).
Quick checklist
- Are production and development dependencies correctly split between
dependenciesanddevDependencies? - Is
package-lock.jsoncommitted alongsidepackage.json? - Are the scripts used most often in day-to-day work documented in
scripts? - If the project is internal and shouldn’t be published, is
"private": trueset?
If you need to quickly validate a package.json‘s syntax before committing, paste it into JSON Validator.