---
title: "03 — npm — The Package Manager That Grew the Ecosystem"
uid: npm
tags: ["workspaces", "nodejs", "npx", "semver", "roadmap:nodejs", "npm", "packages"]
excerpt: "npm is three things in one trench coat — a package manager, a registry, and a script runner — held together by semantic versioning."
date: 2026-08-13T03:27:57+0000
source: https://www.aveshina.my.id/en/blog/npm
---

"Just install stuff" was my npm mental model, and it hid the three jobs under one name. The idea that everything else hangs off: **npm is three things in one trench coat** — a package manager (the npm CLI), a registry (the public database at npmjs.com), and a script runner. And the whole thing only works because every package agrees on one versioning convention [1].

The framing that finally landed for me is the layering — registry at the bottom, versioning as the social contract, and the CLI doing the actual work on top.

## The registry and the package manager

The **registry** is a giant public database of packages. Anyone can publish to it, and that openness is why npm became the largest package ecosystem in any language — there is a package for almost everything. The **package manager** is the npm CLI that downloads packages from that registry into my project [1][2].

```
npm install express
```

That command does three things: it downloads express (and everything express depends on, recursively), it writes those files into node_modules/, and it records the dependency in package.json. The lockfile, package-lock.json, pins the exact version and integrity hash of every package so that the same install reproduces on another machine.

## Local versus global installation

Packages get installed in one of two scopes, and the difference is just "who can use it":

- **Local** (the default) — the package lands in the project's node_modules/ and is only importable from that project. This is what I want for libraries my code depends on [3].
- **Global** (with -g) — the package lands in a system-wide location and its bin is on my PATH. This is for CLI tools I want to run from anywhere, like a scaffolder or a formatter [4].

```
npm install -g typescript   # global — tsc command available everywhere
npm install express         # local — only this project can import it
```

The rule I use: if my code imports it, install it locally. If my terminal runs it as a command across many projects, install it globally. Mixing the two is the source of most "works on my machine" bugs.

## npx: run without installing

npx is the third tool in the trench coat. It runs a package without permanently installing it — it downloads the package to a temporary cache, executes it, and moves on [5]. The classic use case is one-off CLI tools:

```
npx create-next-app my-app
```

I get the latest create-next-app, it scaffolds the project, and I never have a stale global install sitting around. npx is also how binaries from local node_modules/.bin get invoked cleanly, without me typing the full path.

## Semantic Versioning: the social contract

The single idea that makes any of this work is **SemVer** — Semantic Versioning. Every package version is three numbers, MAJOR.MINOR.PATCH, and each number has a precise meaning [6]:

- **MAJOR** — breaking changes. Code that worked on 3.x may not work on 4.0.
- **MINOR** — new features, backwards compatible. Safe to upgrade.
- **PATCH** — bug fixes, backwards compatible. Safe to upgrade.

```
3.7.2
│ │ │
│ │ └── PATCH — bug fix
│ └──── MINOR — new feature
└────── MAJOR — breaking change
```

The reason this matters is the package.json range syntax. "express": "^4.18.0" means "any 4.x that is at least 4.18.0" — the caret (^) allows MINOR and PATCH bumps but blocks MAJOR bumps, protecting me from breaking changes. "~4.18.0" (tilde) is stricter — only PATCH bumps. An exact "4.18.0" pins everything. The lockfile then freezes the specific version that got installed, so ranges are advisory at install time but exact at runtime.

## Updating packages

Because new versions ship constantly, npm gives me tools to move forward safely. npm update bumps packages within their allowed ranges (respecting the ^ or ~). npm outdated shows me what is behind. For jumping a MAJOR version — the breaking one — I reach for npm-check-updates (ncu), which rewrites the ranges in package.json itself, after which I read the changelog carefully and run my tests [7].

## Running scripts

package.json has a "scripts" field, and npm doubles as a task runner [8]:

```
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "test": "vitest"
  }
}
```

npm run dev executes the dev script. A few script names (start, test, install) are special — they run with npm start / npm test, no run needed. Scripts can call each other, and pre/post hooks (pretest, postbuild) run automatically around their parent. This is the lightest-weight build system I know, and most projects use it as their single entry point.

## Workspaces and creating packages

Two more capabilities round out the tool. **npm workspaces** let a single root package.json manage multiple packages in a monorepo (one repository holding many packages) — npm install at the root links the internal packages together, and a script can be run across every workspace at once [9]. And **publishing** my own package is the inverse of installing — npm publish uploads the package to the registry (or a private one), where anyone can install it [10]. The boundary between consumer and producer is thin, which is exactly why the ecosystem grew so fast.

## How I use this

The habit I keep is to read the version range, not just the package name. When something breaks after an install, the first thing I check is what actually landed in the lockfile — because ^ lets MINOR bumps through, and a transitive dependency's MINOR bump can still surprise me. For production I treat the lockfile as the source of truth (npm ci for clean installs from the lockfile), and I bump deliberately, reading changelogs for MAJOR jumps. The model — registry, manager, versioning — is what turns "install stuff" into something I can reason about.

## References

[1] OpenJS Foundation, "An introduction to the npm package manager," nodejs.org, 2024. [Online]. Available: [https://nodejs.org/en/learn/getting-started/an-introduction-to-the-npm-package-manager](https://nodejs.org/en/learn/getting-started/an-introduction-to-the-npm-package-manager)

[2] npm, Inc., "npm Documentation," docs.npmjs.com. [Online]. Available: [https://docs.npmjs.com/](https://docs.npmjs.com/)

[3] npm, Inc., "Downloading and installing packages locally," docs.npmjs.com. [Online]. Available: [https://docs.npmjs.com/downloading-and-installing-packages-locally](https://docs.npmjs.com/downloading-and-installing-packages-locally)

[4] npm, Inc., "Downloading and installing packages globally," docs.npmjs.com. [Online]. Available: [https://docs.npmjs.com/downloading-and-installing-packages-globally](https://docs.npmjs.com/downloading-and-installing-packages-globally)

[5] npm, Inc., "npx," docs.npmjs.com. [Online]. Available: [https://docs.npmjs.com/cli/commands/npx/](https://docs.npmjs.com/cli/commands/npx/)

[6] T. Preston-Werner, "Semantic Versioning 2.0.0," semver.org. [Online]. Available: [https://semver.org/](https://semver.org/)

[7] npm, Inc., "Updating packages downloaded from the registry," docs.npmjs.com. [Online]. Available: [https://docs.npmjs.com/updating-packages-downloaded-from-the-registry](https://docs.npmjs.com/updating-packages-downloaded-from-the-registry)

[8] npm, Inc., "Using npm scripts," docs.npmjs.com. [Online]. Available: [https://docs.npmjs.com/cli/using-npm/scripts](https://docs.npmjs.com/cli/using-npm/scripts)

[9] npm, Inc., "npm workspaces," docs.npmjs.com. [Online]. Available: [https://docs.npmjs.com/cli/using-npm/workspaces](https://docs.npmjs.com/cli/using-npm/workspaces)

[10] Snyk, "Best practices for creating a modern npm package," Snyk Blog. [Online]. Available: [https://snyk.io/blog/best-practices-create-modern-npm-package/](https://snyk.io/blog/best-practices-create-modern-npm-package/)

```quiz
Q: What does the caret (`^`) in `"express": "^4.18.0"` allow?
- Any version, including breaking majors
- MINOR and PATCH updates within the same MAJOR (4.x.x), blocking breaking MAJOR bumps
- Only exact PATCH updates (4.18.x)
correct: 1
explain: The caret allows backwards-compatible updates — MINOR and PATCH bumps — but blocks MAJOR bumps, which signal breaking changes. Tilde (~) is stricter, allowing only PATCH.

Q: When should you install a package globally with `-g`?
- When your code imports it
- When you want to run it as a CLI command across multiple projects
correct: 1
explain: Global installs put the package's bin on your PATH so it runs from anywhere. Code dependencies belong in a local node_modules so the project is self-contained and reproducible.

Q: What does `npx create-next-app my-app` do?
- Permanently installs create-next-app as a global tool
- Downloads create-next-app to a temporary cache, runs it, and does not leave a global install behind
correct: 1
explain: npx runs a package without permanently installing it — it fetches to a cache, executes, and moves on. Useful for one-off scaffolders and CLI tools you don't want cluttering the global environment.

Q: Which command performs a clean install strictly from the lockfile (used in CI)?
- npm install
- npm ci
correct: 1
explain: npm ci wipes node_modules and installs exactly what the lockfile pins, faster and more reproducible than npm install (which may rewrite the lockfile). It's the right choice for CI and production deploys.

Q: In SemVer, bumping the MAJOR number signals…
- a backwards-compatible new feature
- a breaking change — existing code may need updates
- a bug fix
correct: 1
explain: MAJOR bumps are reserved for breaking changes. MINOR is for backwards-compatible features, PATCH for backwards-compatible fixes.
```
