Learn what package.json is, how to create one, and what each important field does, from name, version and type to scripts, dependencies, exports and engines.

Bishal Biswas
Self Learner
What is package.json?
package.json is a small file in the top folder of every Node project. It describes the project: its name, which packages it needs, how to run it, and which Node versions it supports.
Think of it as the label on a moving box. Anyone, person or tool, can read the label and know what is inside and how to handle it, without unpacking everything.
npm reads it to install packages. Node reads it to decide how to load your files. Other developers read it to understand your project in seconds.
Creating one
Inside your project folder, run:
npm init -y
The -y means “yes to all the default answers”. Without it, npm asks you questions one by one. You get this file:
{
"name": "notes-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs"
}
The name comes from your folder name. Notice the last line: npm init sets "type": "commonjs". This course uses ES modules, so change it straight away:
npm pkg set type=module
npm pkg set edits package.json for you, so you cannot accidentally break the JSON.
It must be real JSON
package.json is JSON, not JavaScript. JSON is stricter:
- Keys and strings need double quotes.
- No comments.
- No trailing comma after the last item.
{
"name": "notes-app",
"version": "1.0.0",
}
That trailing comma breaks every npm command:
npm error code EJSONPARSE
npm error JSON.parse Note: package.json must be actual JSON, not just JavaScript.
The identity fields
name
The project’s name. It must be lowercase, with no spaces. Use hyphens instead: notes-app, not Notes App.
Packages published to npm can also have a scope, a prefix that groups packages under a user or organisation: @asha/notes-app.
version
The project’s version in MAJOR.MINOR.PATCH form, such as 1.0.0. It matters most when you publish a package. Semantic Versioning and Lockfiles explains the rules.
description, keywords, author, license
Information for humans and for the npm website. license says how others may use your code, using a short code such as MIT or ISC.
private
{
"private": true
}
"private": true stops npm from ever publishing the project to the public registry. Add it to every app that is not meant to be a shared package.
How Node loads your code
type
As you saw in CommonJS vs ES Modules, "type" decides how Node treats .js files:
"type": "module":.jsfiles are ES modules (import/export)."type": "commonjs", or notype:.jsfiles are CommonJS (require).
main and exports
These say which file someone gets when they import your package by name.
main is the older, simpler field. It names a single entry file:
{
"main": "src/index.js"
}
exports is the modern field. It can list several entry points, and it hides every file you do not list:
{
"name": "notes-kit",
"exports": {
".": "./src/index.js",
"./format": "./src/format.js"
}
}
With this, users can write:
import { addNote } from 'notes-kit'; // loads src/index.js
import { formatNote } from 'notes-kit/format'; // loads src/format.js
But anything else is blocked:
import 'notes-kit/src/internal.js';
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './src/internal.js' is not defined by "exports"
That protection lets package authors reorganise private files without breaking anyone. For an app you only run yourself, you do not need either field.
scripts
Scripts are named shortcuts for commands you run often:
{
"scripts": {
"start": "node src/index.js",
"dev": "node --watch src/index.js",
"test": "node --test"
}
}
Run them with npm run dev, or the faster node --run dev. npm, npx and node –run covers scripts in detail.
Dependencies
A dependency is a package your project uses. When you install one, npm records it here.
dependencies
Packages your app needs to run:
npm install express
{
"dependencies": {
"express": "^5.1.0"
}
}
devDependencies
Packages you need only while developing, such as linters, formatters and build tools. Install them with --save-dev (or -D):
npm install --save-dev prettier
{
"devDependencies": {
"prettier": "^3.6.2"
}
}
When you deploy an app, you can skip devDependencies with npm install --omit=dev, which keeps the install smaller.
What does ^5.1.0 mean?
The ^ (caret) means “version 5.1.0 or any later version that is still 5.x”. It allows bug fixes and new features, but never a new major version that might break your code. The semver lesson covers all the symbols.
Note: You will also see
peerDependenciesin packages that plug into something else, such as a React component library that expects your app to provide React. You rarely need to write them in an app.
engines
engines declares which Node versions the project supports:
{
"engines": {
"node": ">=24"
}
}
npm warns when someone installs with a version outside this range. Installing Node.js LTS and Version Managers showed how to make it a hard error with engine-strict=true.
Two fields you will meet later
binturns a file in your package into a terminal command. See Building CLIs and Publishing to npm.fileslists which files are included when you publish a package.
Reading package.json from your code
Sometimes a program wants its own name or version, for example to print it with --version. Import the file as JSON:
// src/version.js
import pkg from '../package.json' with { type: 'json' };
console.log(`${pkg.name} v${pkg.version}`); // prints: notes-app v1.0.0
The with { type: 'json' } part is an import attribute. It tells Node to load the file as data, not code.
Handy npm pkg commands
npm pkg get name version # read fields
npm pkg set engines.node=">=24" # add or change a field
npm pkg delete scripts.test # remove a field
Common mistakes
Leaving "type": "commonjs" from npm init
If you write import in .js files, Node complains Cannot use import statement outside a module. Run npm pkg set type=module.
Editing dependencies by hand and not installing
Typing a new package into dependencies does not download it. Run npm install afterwards, or better, add packages with npm install <name> so npm writes the entry for you.
Quick recap
package.jsondescribes your project to npm, Node and other developers.- Create it with
npm init -y, then set"type": "module"for ES modules. - It must be valid JSON: double quotes, no comments, no trailing commas.
name,versionandprivateidentify the project;type,mainandexportscontrol how code loads.scriptsare named commands;dependenciesare needed to run,devDependenciesonly to develop.enginesdeclares supported Node versions.npm pkg getandnpm pkg setread and edit it safely.
// package-json-demo.mjs: create, read and check a package.json
import { writeFile, readFile, mkdir } from 'node:fs/promises';
import path from 'node:path';
import os from 'node:os';
const dir = path.join(os.tmpdir(), 'notes-app');
await mkdir(dir, { recursive: true });
// --- Build the file as a normal object, then save it as JSON
const pkg = {
name: 'notes-app',
version: '1.0.0',
private: true, // never publish by accident
type: 'module', // .js files are ES modules
scripts: { start: 'node src/index.js', dev: 'node --watch src/index.js' },
dependencies: { express: '^5.1.0' }, // needed to run
devDependencies: { prettier: '^3.6.2' }, // needed only while developing
engines: { node: '>=24' },
};
const file = path.join(dir, 'package.json');
await writeFile(file, JSON.stringify(pkg, null, 2) + '\n');
// --- Read it back, exactly as npm would
const loaded = JSON.parse(await readFile(file, 'utf8'));
console.log(`${loaded.name} v${loaded.version} (${loaded.type})`);
console.log('Scripts you can run:');
for (const [name, command] of Object.entries(loaded.scripts)) {
console.log(` node --run ${name.padEnd(6)} -> ${command}`);
}
console.log('Runtime packages:', Object.keys(loaded.dependencies).join(', '));
console.log('Dev-only packages:', Object.keys(loaded.devDependencies).join(', '));
// --- A simple engines check for ">=N" ranges
const wanted = Number(loaded.engines.node.replace('>=', ''));
const current = Number(process.versions.node.split('.')[0]);
console.log(current >= wanted
? `Node ${current} satisfies "${loaded.engines.node}"`
: `Node ${current} is too old, "${loaded.engines.node}" is required`);
// --- Broken JSON is the most common package.json mistake
try {
JSON.parse('{ "name": "notes-app", }');
} catch (error) {
console.log('Trailing comma breaks it:', error.message);
}
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready