package.json, npm, and Scripts

Published Updated

By the end of this page you will know what package.json is for, which of its fields matter early, why dependencies are split into two lists, how to turn a long command into a short named one, what a lockfile protects you from, and when npx is the right tool.

This is the site's single home for package.json, npm, scripts, and lockfiles. Other pages link here rather than explaining them again, which is deliberate: one file this central should be described in one place.

A note on evidence, because this page mixes two kinds of claim. The behaviours Node itself decides from this file were run on Node 24.16.0 and carry a labelled output block. The npm commands are described from npm's own documentation and were not run here, so no output is shown for them and none is invented.

What package.json Actually Is

It is a plain JSON file sitting at the top of a project, and two different audiences read it. Node reads a small number of fields when it runs your code, and npm reads the rest when it installs or runs things.

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 24.16.0 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.

The Fields Worth Knowing

A generated file has more fields than you need on day one. These are the ones that do visible work:

{
  "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 24.16.0 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.

Dependencies Against devDependencies

Two lists, one question: does the running program need this, or only the person working on 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 the deployed program never imports.

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. Moving an entry between the two objects is all it takes to correct one.

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.

Scripts, Naming the Commands You Keep Retyping

The scripts object maps a short name to a command line. It exists so that nobody has to remember the long form or keep it in a text file somewhere:

{
  "scripts": {
    "start": "node index.js",
    "clean": "node fix-links.js posts",
    "check": "node --check index.js"
  }
}

Run one with npm run clean. 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.

The second is that a script runs with the project's locally installed tools already 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.

The Lockfile and Why It Is Committed

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 actually installed, including the ones your dependencies pulled in themselves.

It is not automatically frozen, though. npm install honours 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.

npx and Running Something Once

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, and it is the reason no npx command was run while writing this page. 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 actually 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?

No. It is large, machine-specific, and rebuildable from the manifest and the lockfile, which is exactly why those two are committed instead. Add it to your gitignore file on the first day, because removing it from history afterwards is far more work.

What does the caret in a version number mean?

A caret accepts later releases that keep the same leftmost non-zero number, so it treats them as compatible. That flexibility is why two people can resolve different versions from the same manifest. Pinning exact versions removes the guesswork, and it is the safer default when a surprise upgrade would be expensive.

Why does a fresh install give different versions to a teammate?

Almost always because the lockfile was not committed, so each install resolved the ranges independently. Commit it, and use npm ci where the tree must match exactly, since plain npm install re-resolves whenever the lock no longer satisfies package.json. The other cause is two people using different package managers.

Do you need package.json for a single script?

No, a single file runs perfectly well without one. Add it as soon as you want import and export declared rather than detected, a named command instead of a memorised one, or your first dependency. Those three needs tend to arrive together.

Is npm the only option?

No. npm ships with Node, and pnpm and Yarn read the same manifest while storing files and resolving versions differently. Pick one per project and stay with it, because mixing them produces two lockfiles that disagree about the same tree.

Sources

  1. [1]
    Modules: Packages
    (nodejs.org)
  2. [2]
    package.json
    (docs.npmjs.com)
  3. [3]
    npm-run-script
    (docs.npmjs.com)