Skip to content

package.json Deep Dive

package.json is the manifest for every Node.js project. It tells Node and npm who owns the package, what version it is, how to run it, which files are public API, and which packages it depends on. Every npm install, npm run, and npm publish command reads from this file.

FieldPurpose
nameThe package identifier on the npm registry (lowercase, URL-safe)
versionThe current release, following Semantic Versioning (MAJOR.MINOR.PATCH)
typeModule system default: "module" for ESM, "commonjs" (or absent) for CJS
mainLegacy entry point for CJS consumers (require('your-pkg'))
exportsModern entry point map — controls what consumers can import and from which subpaths
scriptsNamed shell commands run with npm run <name> (plus lifecycle hooks like start, test)
{
"name": "my-library",
"version": "1.2.0",
"description": "A small utility library",
"type": "module",
"main": "./dist/index.cjs",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./utils": "./dist/utils.js"
},
"scripts": {
"build": "tsc",
"test": "node --test",
"start": "node dist/index.js",
"lint": "eslint src"
},
"dependencies": {
"zod": "^3.22.4"
},
"devDependencies": {
"typescript": "~5.4.0"
}
}

The exports field takes precedence over main in modern Node.js. It lets you expose multiple subpaths and serve different files to ESM and CJS consumers from the same package.

npm uses Semantic Versioning for dependency ranges. The range syntax in package.json controls which versions npm install considers compatible.

RangeMeaningExample
^1.2.3Compatible — allows MINOR and PATCH bumps, locks MAJOR>=1.2.3 <2.0.0
~1.2.3Patch-only — allows only PATCH bumps>=1.2.3 <1.3.0
1.2.3Exact — only this version1.2.3
*Any version — no constraint>=0.0.0
{
"dependencies": {
"express": "^4.18.2",
"dotenv": "~16.3.1",
"uuid": "9.0.0",
"some-experimental-pkg": "*"
}
}

In practice, ^ is the most common choice for dependencies: it keeps you on the latest compatible release while protecting against breaking changes introduced by a new major version.

npm init walks you through an interactive prompt to generate a package.json. npm init -y skips all prompts and accepts the defaults — useful when you want a minimal file fast.

Terminal window
# Interactive wizard
npm init
# Accept all defaults immediately
npm init -y

After running npm init -y you get a minimal package.json in the current directory:

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

Edit the file directly or use npm pkg set name="my-library" type="module" to update individual fields from the terminal.

Which package.json field is recommended over "main" in modern Node.js for controlling what a package exposes?
You want to allow patch-level updates to a dependency but prevent minor or major bumps. Which semver range prefix should you use?
What does "npm init -y" do?