AV
HomeAboutProjectBlog

© 2026 Ave syah Shina. All rights reserved.

  1. Home
  2. Blog
  3. 03 — npm — The Package Manager That Grew the Ecosystem

03 — npm — The Package Manager That Grew the Ecosystem

August 13, 20266 min read
Download as Markdown

"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

[2] npm, Inc., "npm Documentation," docs.npmjs.com. [Online]. Available: 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

[4] npm, Inc., "Downloading and installing packages globally," docs.npmjs.com. [Online]. Available: 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/

[6] T. Preston-Werner, "Semantic Versioning 2.0.0," semver.org. [Online]. Available: 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

[8] npm, Inc., "Using npm scripts," docs.npmjs.com. [Online]. Available: 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

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

Knowledge check · Question 1 of 5

What does the caret (`^`) in `"express": "^4.18.0"` allow?

Comments

Leave a Comment

You must be signed in to comment

0 Comments

No comments yet. Be the first to comment!