Node.js package.json, npm, and Scripts Explained
package.json describes a Node.js project, its dependencies, and the commands you run regularly. npm uses it to install packages and run named scripts, while Node reads fields such as type to interpret your files. If you are new to running JavaScript outside a browser, start with the Node.js guide.
What Is package.json?
A package.json file stores a project’s package details and configuration as JSON. Node reads runtime settings such as the module type; npm reads dependency lists and script commands.
You can create one by hand. npm init -y writes a starter version for you, filling in the folder name and sensible defaults, and either route gives you the same kind of file.
Because it is JSON, the rules are strict: double quotes on every key, no trailing commas, and no comments. Break one of those and Node stops before your code runs at all:
// node output, with a trailing comma in package.json:
// Error: Invalid package config /path/to/package.json.
// exit code: 1 The error names the config file rather than any line you wrote, which is the clue. Nothing in your JavaScript is wrong.
Which package.json Fields Do You Need?
The main package.json fields to understand are type, scripts, dependencies, and devDependencies. The name and version fields identify the package.
{
"name": "notes-cleaner",
"version": "1.0.0",
"type": "module",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {},
"devDependencies": {}
} name and version identify the project, and only matter to anyone else if you publish it. main names the entry file for code that imports your project as a package, which a private script never does.
type is the field Node itself acts on, and it decides whether the .js files in this package use import or require. It is not the only thing that can decide: a .mjs or .cjs extension overrides it, and syntax detection settles files it never covered. The running guide covers both.
Your own code can read the file too, with an import attribute:
import pkg from "./package.json" with { type: "json" };
console.log("type field:", pkg.type); // node output:
// type field: module That with { type: "json" } spelling became stable in Node 20.18.3 and 22.12, after arriving experimentally in 20.10. Older material shows assert { type: "json" } instead, which Node 22 removed, so treat it as a spelling you may meet rather than one to copy.
How Do dependencies and devDependencies Differ?
The dependencies list contains packages needed at runtime; devDependencies contains tools used to develop, test, or build the project. Build tools may still be needed on a deployment service that builds your app before serving it.
dependencies holds packages your code imports and therefore needs wherever it runs. devDependencies holds the tooling around it, such as test runners, linters, and type checkers, which are usually unnecessary once the application has been built.
The distinction earns its keep in two places. A production install can skip the development list with npm install --omit=dev, or by setting NODE_ENV=production, which makes that the default. The split also documents intent for the next person reading the file.
npm install <name> adds to the first list and npm install --save-dev <name> adds to the second. If you move an entry between the lists, run npm install afterward to update the lockfile too.
Get it wrong in one direction and it bites in production. Something your code imports at runtime, filed under devDependencies by a stray --save-dev, is exactly what the production install above leaves out, so the deployed program dies with a module-not-found for a package that worked perfectly on your machine.
One habit is worth adopting before you add anything. Check what you are installing. A package name is easy to typo, easy to imitate, and installing one can run its own code on your machine, so read the package's own page rather than a forum post.
How Do You Create and Run npm Scripts?
To create an npm script, add a named command to the scripts object in package.json. Run it with npm run followed by its name, such as npm run build.
{
"scripts": {
"start": "node index.js",
"clean": "node fix-links.js posts",
"check": "node --check index.js"
}
} Run one with npm run clean. The special names start and test also have shortcuts, npm start and npm test. Running npm run with no name lists the scripts that exist, which makes the file itself the documentation for how a project is operated.
Two details save confusion. Arguments meant for your script go after a double dash, as in npm run clean -- --dry-run, because otherwise npm reads them as its own.
npm runs scripts from the package root, even if you invoke npm from a subfolder. It also puts the project's locally installed tools on the path. A tool installed as a dependency can therefore be named directly rather than by its full path.
Scripts also pass through the exit code of the command they ran, which is why the exit-code discipline from the previous guide matters here. A failing script stops a chain of them rather than letting it continue quietly.
Why Should You Commit package-lock.json?
Commit package-lock.json so teammates and automated builds can install the dependency versions recorded for your project. Version numbers in package.json are frequently ranges rather than exact values. A caret accepts later releases that keep the same leftmost non-zero number, which means the same manifest can resolve to different versions on different days.
The lockfile removes most of that variability. It records the exact version of every package that was installed, including the ones your dependencies pulled in themselves.
It is not automatically frozen, though. npm install honors the locked versions only while they still satisfy package.json, and re-resolves and rewrites the lock when they do not. npm ci is the strict command: it installs exactly what the lockfile says and fails outright when the two disagree.
Three consequences follow, and they are the whole of the practical advice. Commit the lockfile, since a teammate without it gets a different tree from the same manifest. Do not hand-edit it, because the tool regenerates it.
And use one package manager per project. npm, pnpm, and Yarn each write their own lockfile format, and two of them disagreeing about one tree is worse than either alone.
node_modules is the opposite case: large, machine-specific, and rebuildable from those two files. It belongs in your gitignore file from the first day.
When Should You Use npx?
npx ships with npm and runs a package's command without adding it to your project. It prefers a locally installed copy and otherwise fetches the package to run it, which is the point when you want a scaffolding tool once and never again.
That convenience is also the caution. The command executes code from a package you may never have looked at.
So it deserves the same scrutiny as an install. Check the exact name against the project's own documentation, since a near-miss spelling is the oldest trick in this ecosystem.
For anything you run more than twice, prefer a named script over a remembered npx line. The script is committed, reviewable, and identical for everyone on the project.
Pitfalls and Debugging
Invalid package config. The JSON is malformed, usually a trailing comma or a missing quote, and Node reports it before running anything of yours. Open the file and check the punctuation rather than the code.
npm run says the script is missing. Either the name is spelled differently in the file, or the terminal is not in the folder holding that package.json. Running npm run with no name lists what is there.
Your flags reached npm instead of your script. Without the double dash, npm treats them as its own arguments and your script sees nothing. npm run clean -- --dry-run is the form that passes them through.
Two lockfiles in one repository. Someone installed with a different package manager. Decide which one the project uses, delete the other lockfile, and say so in the readme.
A dependency that only the tooling needs. Nothing breaks, but a production install carries weight it does not use. Move the entry into devDependencies.
Works locally, module-not-found once deployed. The opposite mistake: something imported at runtime sits in devDependencies, so a production install omits it. Check which list names the package before hunting the deployment.
A dependency added because one line of it was useful. Every package is code you now trust and maintain. Check whether the standard library already covers it before the install, not after.
Frequently Asked Questions
Should node_modules go into version control?
Do not normally commit node_modules. Commit package.json and the lockfile instead, then reinstall the dependencies when needed. Add node_modules to .gitignore to keep installed packages out of new commits.
What does the caret in an npm dependency version mean?
An npm caret range allows updates that keep the same leftmost non-zero version component. For example, ^1.2.3 permits versions below 2.0.0, while ^0.2.3 permits versions below 0.3.0. A committed lockfile records the exact versions selected for an install.
Why can npm install different dependency versions for teammates?
npm can install different dependency versions when teammates use different lockfiles, package settings, or npm versions. Share the lockfile and use npm ci for a clean install that follows it. Platform-specific optional packages can still differ between operating systems.
Do you need package.json for a single Node.js script?
A single Node.js script can run without package.json. Add the file when you need dependencies, named npm commands, or an explicit module type for .js files. A standalone .mjs file can use ES modules without that setting.
Is npm the only Node.js package manager?
npm is not the only Node.js package manager. pnpm and Yarn also use package.json, but manage installation and lockfiles differently. Follow the package manager already chosen for a project.
Sources
-
[1]
Modules: Packages(nodejs.org)
-
[2]
package.json(docs.npmjs.com)
-
[3]
npm-run-script(docs.npmjs.com)
Read Next
Reading and writing files with fs/promises, paths that survive the wrong working folder, keeping keys out of code with env files, and one worked bulk edit with a dry run.
Checking your install and reading version numbers, running a file, what type module changes, reading arguments from process.argv, the two output streams, and exit codes.
JavaScript run on your own machine: what Node is, the handful of terminal commands you actually need, and the three guides covering running files, package.json, and file automations.