npm is the default package manager for Node.js, and it’s your primary tool for bringing in external code to your projects.
Let’s see it in action. Imagine you’re starting a new Node.js project. You’d create a directory, cd into it, and then initialize npm to create a package.json file. This file is the manifest for your project, tracking its metadata and dependencies.
mkdir my-awesome-project
cd my-awesome-project
npm init -y
The -y flag is a shortcut that accepts all the default prompts, quickly creating a package.json like this:
{
"name": "my-awesome-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Now, let’s add a popular utility library, lodash, to our project. We’ll use the install command, often shortened to i.
npm install lodash
When you run this, npm does a few things:
- It looks up
lodashin the npm registry. - It downloads the package and its own dependencies into a
node_modulesfolder within your project. - Crucially, it updates your
package.jsonto includelodashunder thedependenciessection.
Your package.json will now look something like this:
{
"name": "my-awesome-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"lodash": "^4.17.21"
}
}
The ^4.17.21 is a semantic versioning (semver) range. The caret ^ means npm will install the latest minor or patch version (e.g., 4.17.22 or 4.18.0) but not a new major version (like 5.0.0). This helps ensure backward compatibility.
You can also install packages for development purposes, meaning they aren’t needed for your application to run in production. Think of testing frameworks or build tools. You use the --save-dev (or -D) flag for this. Let’s install jest for testing:
npm install jest --save-dev
This will add jest to a new section in your package.json: devDependencies.
{
"name": "my-awesome-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"lodash": "^4.17.21"
},
"devDependencies": {
"jest": "^29.7.0"
}
}
When you want to install all the dependencies listed in package.json for a project (e.g., after cloning a repository), you simply run:
npm install
This command reads package.json and downloads all packages listed in dependencies and devDependencies into the node_modules folder. If a package-lock.json file exists, npm install will use that to install the exact versions specified, ensuring reproducible builds across different environments.
One often overlooked aspect of npm install is its behavior with package-lock.json. When present, this file dictates the exact versions of every package, including all transitive dependencies (dependencies of dependencies), that should be installed. This is critical for preventing "it works on my machine" problems by ensuring everyone uses the same dependency tree. npm install will only update package-lock.json if you explicitly add, update, or remove packages via npm install <package-name> or npm uninstall <package-name>.
The next step after managing dependencies is often running scripts defined in your package.json or understanding how to publish your own packages.