package.json and npm basics

Beginner40 min

Start a project with npm init, add a dependency, understand package.json and package-lock.json and semver ranges, and run scripts with npm run and node --run.

What you will learn
  • Create a project with package.json and set its module type
  • Install a dependency and a development dependency
  • Read a semver range such as ^2.1.3
  • Run a project script with npm run and node --run
On this page

A real project is more than a few files. It has a name, a list of other people’s code it depends on, and commands to run it. Node.js projects keep all of that in a file called package.json. npm, the package manager that is installed with Node.js, reads and writes that file.

The commands in this lesson were run with Node.js 26.11.1 and npm 11.20.0. They work in the same way on the Node.js 22 and 24 lines, but the exact files and messages npm prints can differ between npm versions.

Create a project

Make a folder, move into it and run npm init. The -y flag accepts the defaults for every question.

Terminal window
mkdir greeter
cd greeter
npm init -y

npm writes a package.json that looks like this (the name comes from the folder):

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

Two fields matter right away. "name" and "version" identify the project. "type" decides how Node.js reads .js files: "commonjs" is the older module system, and "module" makes .js files ES modules, as the last lesson described. This course uses ES modules, so change it. Edit the file, or run npm pkg set type=module.

Here is the package.json for a small project we will use. It is "private" so it cannot be published by accident, has "type": "module" and defines two scripts:

package.json
{
"name": "greeter",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"start": "node src/hello.mjs",
"dev": "node --watch src/hello.mjs"
}
}

Its single source file is:

hello.mjs
const name = process.argv[2] ?? "world";
console.log(`Hello, ${name}!`);
Output
Hello, world!

process.argv is an array of the words typed on the command line after node. Item 0 is the path of Node itself and item 1 is the path of your script, so process.argv[2] is the first word you added yourself. When you add none, it is undefined, and ?? supplies "world". Run node hello.mjs Ada and it prints Hello, Ada!.

Scripts

The "scripts" field gives names to commands. Run one with npm run NAME. The name start is special, so npm start works too.

Terminal window
npm run start
Output
> greeter@1.0.0 start
> node src/hello.mjs
Hello, world!

npm prints the command it runs, then the program’s output. Node.js can also run a script by itself, without npm:

Terminal window
node --run start
Output
Hello, world!

node --run was added in Node.js 22.0.0. Here it prints no extra lines. It runs the script’s command and nothing else, so it is a good fit for simple scripts like these. To pass arguments to the script, put them after --: node --run start -- Ada prints Hello, Ada!.

The dev script uses node --watch, which restarts the program every time a file it uses changes. Stop it with Ctrl+C.

Add a dependency

A dependency is a package that your project uses. Packages come from the npm registry. To add one, run npm install NAME. Here is a small package called ms, which converts time strings to milliseconds:

Terminal window
npm install ms
Output
added 1 package, and audited 2 packages in 461ms
found 0 vulnerabilities

Three things changed:

  1. package.json has a new "dependencies" section: "ms": "^2.1.3".
  2. node_modules/ is a new folder holding the package’s files. Do not edit it, and do not commit it to version control.
  3. package-lock.json is a new file that records the exact version of every package that was installed.

Now use the package. This file is index.js and the project has "type": "module":

import ms from "ms";
console.log(ms("2 days"));
console.log(ms(60000));
console.log(ms(3600000, { long: true }));
Output
172800000
1m
1 hour

import ms from "ms" has no ./, so Node looks in node_modules for a package named ms. The first call converts text to milliseconds, and the others convert back.

Dependencies and development dependencies

Some packages are needed only while you work on the project, such as a code formatter or a test tool. Install those with --save-dev (or -D):

Terminal window
npm install --save-dev prettier

They go in a separate "devDependencies" section:

"dependencies": {
"ms": "^2.1.3"
},
"devDependencies": {
"prettier": "^3.9.10"
}

dependencies are needed when the program runs. devDependencies are needed only for development. A server can install only the first group with npm ci --omit=dev.

Semver ranges

Package versions follow semantic versioning: three numbers, MAJOR.MINOR.PATCH. A patch release fixes bugs, a minor release adds features without breaking existing use, and a major release may break things. The text before the version in package.json is a range that tells npm which versions are acceptable:

Written Allows
2.1.3 exactly 2.1.3
^2.1.3 2.1.3 up to, but not including, 3.0.0 (what npm install writes by default)
~2.1.3 2.1.3 up to, but not including, 2.2.0

For versions that start with 0, the caret is stricter: ^0.2.3 allows only 0.2.x from 2.3, because anything below 1.0.0 may change at any time.

The lock file and npm ci

A range means two people can install different versions on different days. The lock file, package-lock.json, prevents that. It records the exact version of every package, including the packages your packages need. Commit it to version control together with package.json.

  • npm install installs what the lock file says, and updates the lock file when you add or change a dependency.
  • npm ci (“clean install”) deletes node_modules and installs exactly what the lock file lists. It fails if package.json and the lock file disagree. Use it on build servers and in tests, where you want the same result every time.
Terminal window
npm ci
Output
added 2 packages, and audited 3 packages in 361ms

To see what is installed, run npm ls. To remove a package, run npm uninstall NAME.

Common pitfalls

  • Committing node_modules. It is large and anyone can recreate it from package.json and the lock file. Keep it out of version control.
  • Forgetting the lock file. Commit package-lock.json.
  • Editing package.json by hand and forgetting npm install. The lock file and node_modules fall out of date.
  • Leaving "type": "commonjs" and using import. The "type": "commonjs" that npm init -y writes means exactly that: import in a .js file is a syntax error. Set "type": "module", or use .mjs. (With no "type" at all, Node 22.7 and later guesses by syntax detection, as the ES modules lesson explained, but it warns on each run. Set the field.)
  • Installing a package you do not recognise. Check its name and its publisher first. Installing runs other people’s code on your machine.

Exercises

ExerciseCreate a project with one dependency and one script

Make a new folder and turn it into an ES-module project. Install ms, create index.js that prints ms("1 hour"), and add a script called start that runs it. Run it with both npm start and node --run start. Expected result: both commands print 3600000.

Show solution
Terminal window
mkdir timer
cd timer
npm init -y
npm pkg set type=module scripts.start="node index.js"
npm install ms

index.js:

import ms from "ms";
console.log(ms("1 hour"));

Both npm start and node --run start print 3600000. Check package.json: "type" is "module", "ms" appears under dependencies and scripts.start holds the command.

ExerciseRead a range

A project lists "left-pad": "~1.3.0". Would npm accept version 1.3.7? 1.4.0? 2.0.0?

Show solution

1.3.7 is accepted: it is the same minor version, and only the patch differs. 1.4.0 is not accepted, because ~ allows patch changes only. 2.0.0 is not accepted either. With ^1.3.0 instead, both 1.3.7 and 1.4.0 would be accepted, and 2.0.0 would not.

Summary

  • package.json describes a project: its name, version, module type, scripts and dependencies. npm init -y creates one.
  • Set "type": "module" so .js files are ES modules.
  • npm install NAME adds a dependency, and --save-dev adds a development one. Packages live in node_modules.
  • ^ and ~ allow newer compatible versions. package-lock.json pins exact versions, and npm ci installs exactly those.
  • npm run NAME and node --run NAME run a script from package.json.

Next: loading JSON and code while a program runs.