npm and Packages

Manage a Node.js project with npm: create package.json, install and remove packages, separate dependencies from devDependencies, run scripts, understand semantic versioning and the lock file, and use npx.

Lesson 3 of 43Beginner18 min read

One of the biggest reasons for Node's success is npm, the Node Package Manager. It's installed automatically with Node and gives you access to the npm registry, the largest collection of open-source packages in the world: web frameworks, database drivers, date libraries, testing tools and much more. Instead of writing everything from scratch, you install a well-tested package with a single command.

Every Node project is described by a package.json file. It records the project's name and version, the packages it depends on, and scripts for common tasks such as starting the server or running tests. Anyone who clones your project runs npm install and gets exactly the packages it needs.

In this lesson you'll create a package.json, install and remove packages, understand the difference between dependencies and development dependencies, read version ranges like ^4.21.0, see why the lock file matters, write npm scripts, and run tools with npx. You'll also learn how to judge whether a package is safe to depend on.

Creating a project and installing packages

Terminal
mkdir shop-api && cd shop-api
npm init -y                     # create package.json with defaults
npm install express             # add a dependency
npm install --save-dev vitest   # add a development-only dependency
npm uninstall express           # remove it again
  • Packages are downloaded into the node_modules folder, which can contain thousands of files. Never edit it, and add it to .gitignore so it isn't committed.
  • dependencies are needed to run the app in production; devDependencies are only needed while developing, such as test runners, linters and build tools.
  • npm install with no package name installs everything listed in package.json.

package.json and scripts

JSON
{
  "name": "shop-api",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node src/server.js",
    "dev": "node --watch src/server.js",
    "test": "vitest run"
  },
  "dependencies": { "express": "^5.1.0" },
  "devDependencies": { "vitest": "^3.2.0" }
}
  • Run scripts with npm run dev. npm start and npm test work without run.
  • Scripts can use the tools in node_modules/.bin directly, so vitest works even though it isn't installed globally.
  • "type": "module" makes .js files ES modules, as covered in the modules lesson.

Versions and the lock file

npm packages follow semantic versioning: MAJOR.MINOR.PATCH, such as 5.1.0.

  • PATCH releases fix bugs. MINOR releases add features without breaking anything. MAJOR releases may contain breaking changes.
  • ^5.1.0 allows any 5.x.x version from 5.1.0 up, but not 6.0.0.
  • ~5.1.0 allows only patch updates: 5.1.x.

package-lock.json records the exact version of every installed package, including the packages your packages depend on. Commit it. On servers and in CI, use npm ci, which installs exactly what the lock file says, giving everyone identical installs.

npx and useful commands

  • npx runs a package's command without installing it permanently: npx create-vite@latest my-app.
  • npm outdated lists packages with newer versions; npm update updates within the allowed ranges.
  • npm audit checks your packages against a database of known security problems.
  • npm list --depth=0 shows what's installed at the top level.
  • Global installs (npm install -g) are for command-line tools you use everywhere; project libraries should always be local.

Passing arguments to scripts and pre and post hooks

Scripts often need an extra flag now and then. Anything you type after a double dash is passed straight through to the command the script runs:

Terminal
npm test -- --watch        # runs "vitest run --watch"
npm run dev -- --port 4000

Without the --, npm would try to read --watch as one of its own options.

npm also looks for scripts named after another script with pre or post in front. If package.json has pretest and posttest, then npm test runs pretest, then test, then posttest, stopping if any of them fails. Teams use this for small chores, such as clearing an output folder before a build, but too many hidden hooks make a project hard to follow, so keep them rare.

Typing npm run with no script name lists every script in the project, which is a quick way to learn how an unfamiliar repository is meant to be used.

Node.js Basics Syntax

npm commands

npm init -y
npm install express
npm install -D vitest          # same as --save-dev
npm uninstall lodash
npm install                    # everything in package.json
npm ci                         # exact versions from the lock file
npm run dev
npm test
npx eslint .
npm outdated
npm audit

Node.js Basics Summary

  • npm installs packages from the npm registry into node_modules.
  • package.json lists dependencies, devDependencies and scripts.
  • Run scripts with npm run name; npm start and npm test are shortcuts.
  • Semantic versions are MAJOR.MINOR.PATCH; ^ allows minor and patch updates.
  • Commit package-lock.json and use npm ci for exact, repeatable installs.

Node.js Basics Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Reading package.json from code

Node.js
import { readFile, writeFile } from 'node:fs/promises';

const pkg = {
  name: 'shop-api',
  version: '1.4.2',
  scripts: { start: 'node server.js', test: 'vitest run' },
  dependencies: { express: '^5.1.0', zod: '^3.24.0' },
  devDependencies: { vitest: '^3.2.0' },
};
await writeFile('package-demo.json', JSON.stringify(pkg, null, 2));

const loaded = JSON.parse(await readFile('package-demo.json', 'utf8'));
console.log(`${loaded.name} v${loaded.version}`);
console.log('Scripts:', Object.keys(loaded.scripts).join(', '));
console.log('Runtime packages:', Object.keys(loaded.dependencies).length);
console.log('Dev packages:', Object.keys(loaded.devDependencies ?? {}).join(', '));
Output
shop-api v1.4.2
Scripts: start, test
Runtime packages: 2
Dev packages: vitest
Line-by-line explanation
  1. Lines 3 to 9A typical package.json written out for the demo.
  2. Line 10JSON.stringify with 2 adds indentation, as npm does.
  3. Line 12package.json is plain JSON, so JSON.parse reads it.
  4. Line 16?? {} handles a project with no devDependencies.
Example 2

How version ranges are matched

Node.js
function parse(v) {
  return v.split('.').map(Number);
}

function satisfies(version, range) {
  const [maj, min, pat] = parse(version);
  const [rMaj, rMin, rPat] = parse(range.slice(1));
  const atLeast = maj > rMaj || (maj === rMaj && (min > rMin || (min === rMin && pat >= rPat)));
  if (range.startsWith('^')) return atLeast && maj === rMaj;
  if (range.startsWith('~')) return atLeast && maj === rMaj && min === rMin;
  return version === range;
}

for (const v of ['5.1.0', '5.3.7', '5.1.9', '6.0.0', '5.0.9']) {
  console.log(`${v.padEnd(6)} ^5.1.0: ${satisfies(v, '^5.1.0')}  ~5.1.0: ${satisfies(v, '~5.1.0')}`);
}
Output
5.1.0  ^5.1.0: true  ~5.1.0: true
5.3.7  ^5.1.0: true  ~5.1.0: false
5.1.9  ^5.1.0: true  ~5.1.0: true
6.0.0  ^5.1.0: false  ~5.1.0: false
5.0.9  ^5.1.0: false  ~5.1.0: false
Line-by-line explanation
  1. Lines 1 to 3A version string becomes three numbers.
  2. Line 8The version must be at least the one in the range.
  3. Line 9^ keeps the same major version, allowing new features and fixes.
  4. Line 10~ keeps the same major and minor version, allowing only fixes.
  5. Lines 14 to 166.0.0 is a major release, so neither range accepts it. Real projects use the semver package rather than this simplified version.
Example 3

A project's everyday commands

Terminal
npm init -y
npm install express
npm install -D vitest
npm pkg set scripts.dev="node --watch server.js"
npm run dev
Output
Wrote to /projects/shop-api/package.json
added 66 packages, and audited 67 packages in 3s
added 39 packages, and audited 106 packages in 4s

> shop-api@1.0.0 dev
> node --watch server.js

Server listening on http://localhost:3000
Line-by-line explanation
  1. Line 1-y accepts the default answers, creating package.json straight away.
  2. Line 2express and everything it depends on is installed into node_modules.
  3. Line 3-D records vitest under devDependencies.
  4. Line 4npm pkg set edits package.json from the command line.
  5. Line 5npm run prints the script it's running, then runs it. Package counts vary with versions.

Common Node.js Basics Mistakes

Committing node_modules

Don't
git add .                        # thousands of files from node_modules end up in the repository
Do
echo "node_modules/" >> .gitignore
git add .

Installing a project library globally

Don't
npm install -g express           # the project's package.json doesn't record it
Do
npm install express

Using npm install on the server

Don't
npm install                      # may resolve slightly different versions
Do
npm ci                           # exactly what package-lock.json says

Node.js Basics in Real Projects

npm in professional teams

Teams commit package.json and package-lock.json, run npm ci in continuous integration, and keep scripts like lint, test and build so every developer runs the same commands. Tools such as Dependabot and Renovate open pull requests when packages have updates, and npm audit runs in CI to catch known vulnerabilities. Many companies also use pnpm or Yarn, which work with the same package.json format.

Node.js Basics FAQ

What is the difference between npm install and npm ci?

npm install reads package.json, may update package-lock.json when ranges allow newer versions, and leaves existing node_modules in place. npm ci deletes node_modules, installs exactly what the lock file records, never changes the lock file, and fails if the lock file and package.json disagree. Use install while developing and ci on servers and in CI.

How do I install a specific version of an npm package?

Add the version after an @: npm install express@4.21.2 for an exact version, or npm install express@4 for the newest 4.x release. npm records a range such as ^4.21.2 in package.json by default; add --save-exact (or -E) if you want the exact number written instead.

Should I delete package-lock.json to fix installation problems?

Usually not. Deleting it throws away the exact versions your project was tested with, and the next install may pull in different versions of hundreds of packages at once. Try deleting just node_modules and running npm ci first; if a single package needs updating, run npm install that-package@version so only its entry changes.

What is the difference between npm and npx?

npm manages packages: it installs, removes and updates them and runs the scripts in package.json. npx runs a package's command-line tool, using the copy in your project's node_modules/.bin if there is one and downloading it temporarily if not. For example, npx eslint . runs the project's ESLint without a global install.

Why does npm audit report vulnerabilities in packages I never installed?

Most of the packages in node_modules are dependencies of your dependencies, and npm audit checks all of them. A report doesn't always mean your app is exploitable, because the vulnerable code may never run in the way you use it. Run npm audit fix to apply compatible updates, and look at whether a newer version of the top-level package removes the problem before forcing major upgrades.

Node.js Basics Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the command that runs the "dev" script.

npm  dev
Multiple choiceeasy

Which file lists a project's dependencies and scripts?

Multiple choicemedium

Which versions does "^2.4.1" allow?

Multiple choicemedium

Where should a test runner such as Vitest be installed?

Multiple choicehard

Why use npm ci instead of npm install in continuous integration?

Node.js Basics Quiz

5 questions to check what you learned.

  1. What does npx do?

    • Deletes packages
    • Runs a package's command without a permanent install
    • Publishes a package
    • Updates npm
  2. In version 3.8.2, what does the 8 represent?

    • Major
    • Patch
    • Minor
    • Build
  3. Should package-lock.json be committed to Git?

    • No
    • Only for libraries
    • Only on Windows
    • Yes
  4. Which command checks installed packages for known security problems?

    • npm check
    • npm secure
    • npm verify
    • npm audit
  5. Which scripts can be run without the word run?

    • Any script
    • start and test
    • dev and build
    • None