DEV Community

Cover image for A real-world story: Why we even need npm
Karthik Korrayi
Karthik Korrayi

Posted on

A real-world story: Why we even need npm

Arjun was a junior developer who had just joined a small startup in Bangalore. On his first day, his lead told him: “You’ll build a Task Dashboard for the team – a small Node.js app that tracks tasks, prints useful logs in the terminal, and later exposes APIs for a React frontend.”

By the end of that week, Arjun would discover npm – and realize it’s basically the nervous system of a modern JavaScript project.​


The day Arjun met npm

Arjun started with a plain folder:

mkdir team-task-dashboard cd team-task-dashboard

He opened VS Code, created index.js, and wrote:

console.log("Server is starting...");

He ran:
node index.js

It worked – but it felt boring and fragile. He wanted:

  • Colored logs (green for success, yellow for warnings, red for errors).

  • An HTTP server to later expose APIs.

  • A way to restart automatically when he changed code.

Trying to build all of that from scratch would be like forging every part of a car by hand – mining the metal, melting it, hammering it, assembling it. Instead, his seniors told him: “Use npm. You assemble your app from parts that others already built.”

At this point, Arjun had a question:

“So… what exactly is npm?”


What npm really is (beyond the definition)

His lead explained:

  • Node.js is the runtime that runs JavaScript outside the browser.

  • npm is the package manager that gives you access to a giant library of reusable code (the npm registry).​

  • You talk to npm using the npm command in your terminal (the CLI).

In other words:

  • Authors write packages and publish them to the registry.

  • Developers like Arjun install those packages into their projects using commands like:

    • npm install chalk
    • npm install express
    • npm install --save-dev nodemon

Instead of hand-building everything, Arjun could now:

  • Compose his app out of existing packages.

  • Focus his time on business logic and features.


Installing npm (without realizing you did)

Arjun asked, “Where do I get npm?”

His lead smiled: “You already have it. npm comes with Node.js.”​

When Arjun had installed Node earlier:

  1. He went to https://nodejs.org.

  2. Downloaded the LTS installer for Windows.

  3. Clicked through the wizard – which quietly installed:

-   `node` (the runtime).

-   `npm` (the package manager).[](https://nodejs.org/en/learn/getting-started/an-introduction-to-the-npm-package-manager)​
Enter fullscreen mode Exit fullscreen mode

To confirm, he ran:

node -v
npm -v

Both showed version numbers. Arjun had everything he needed to start playing with npm.​


The first spell: npm init

The startup’s GitHub repo used package.json files everywhere, so Arjun asked, “What is this file?”

His lead answered:

package.json is like your project’s passport and checklist. It describes what the project is and what it depends on.”​

Inside team-task-dashboard, Arjun ran:

npm init -y

This did a few things:

  • Created a package.json file with default metadata.​

  • Told npm, “This folder is now a proper npm project.”

His package.json looked like this:

{
"name": "team-task-dashboard",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"keywords": [],
"author": "Arjun",
"license": "ISC"
}

He didn’t fully understand it yet, but he would soon rely on it heavily.


Adding color to life: installing chalk (local package)

The next pain point: logs. Everything was plain white text. In a long terminal output, it was easy to miss errors. His friend suggested: “Use chalk – it colors console logs.”​

So Arjun ran:

npm install chalk

This single line:

  • Added chalk to dependencies in package.json.

  • Downloaded chalk and many tiny helper packages (like ansi-stylessupports-colorcolor-convertcolor-namehas-flag) into a new node_modules folder.​

  • Updated/created a package-lock.json file with exact versions.​

Now his folder had:

  • package.json

  • package-lock.json

  • node_modules/

  • index.js

In index.js, Arjun updated his code:

import chalk from "chalk";
console.log(chalk.green("Server is starting..."));
console.log(chalk.yellow("Fetching tasks from database..."));
console.log(chalk.red("Error: Database connection failed!"));

Running node index.js now produced colorful logs. A tiny npm package made his app more readable in seconds.​


Local vs global: Arjun’s first confusion

The next requirement was clear:

“When I change the code, I don’t want to stop & restart node index.js every time.”

His mentor recommended nodemon, a tool that restarts the server automatically when files change.​

Arjun saw two options in blog posts:

  • npm install nodemon

  • npm install -g nodemon

He was confused. His mentor explained:

Local packages

  • Installed in the project.

  • Live in node_modules inside the project folder.

  • Listed in dependencies or devDependencies in package.json.​

  • Used by the code via import or require.

Example:

npm install chalk express npm install --save-dev nodemon

Use when:

  • The app itself needs the package to run.

  • You want each project to manage its own versions.

Global packages

  • Installed once, system-wide.

  • Available as commands from any directory.​

  • Usually CLIs or tools, not libraries imported in code.

Example:

npm install -g nodemon

Use when:

  • You want to run a tool like nodemonnpm-check, or serve from any project.​

Mental model Arjun adopted:

  • “If I import it in my code ⇒ install locally.”

  • “If I run it as a global tool in my terminal ⇒ maybe install globally.”

For the project, he chose:

npm install --save-dev nodemon

So that it’s part of the project setup and works for anyone who clones the repo.​


Scripts: teaching npm to do the boring work

package.json also had a scripts field, which was empty. His mentor said:

“Think of scripts as named shortcuts for terminal commands.”

Arjun edited package.json:

{
"name": "team-task-dashboard",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
},
"dependencies": {
"chalk": "^5.2.0",
"express": "^4.18.2"
},
"devDependencies": {
"nodemon": "^3.0.0"
}
}

Now he could run:

npm run dev

  • npm looked at scripts.dev and executed nodemon index.js.​

  • Every time Arjun saved index.jsnodemon restarted the server automatically.

For the production-equivalent run, he used:

npm start

  • start is a special script name that can run with npm start instead of npm run start.​

npm had quietly turned his messy commands into a neat, shareable workflow.


When one package depends on 10 more: the dependency web

One day Arjun opened node_modules and was shocked. He had installed only a few packages, but the folder had hundreds of subfolders.

His mentor laughed:

“Welcome to the dependency universe.”

The reality:

  • Arjun’s project depends on chalk and express.

  • chalk depends on packages like ansi-styles and supports-color.​

  • ansi-styles depends on color-convert.

  • color-convert depends on color-name.

  • express depends on its own long list of packages.

To see the full tree, Arjun ran:

npm list

This printed a hierarchy of dependencies and versions.​

He finally understood why people joke that node_modules is “the densest matter in the known universe”. Each tiny improvement in his app might pull in dozens of small, focused packages.


Versions: Arjun breaks production (in theory)

A few weeks later, Arjun’s app was running smoothly. Then he saw a message:

“New version of chalk available – includes cool new features!”

Curious, he ran:

npm view chalk versions

He saw a long list of versions: 1.0.02.0.03.0.04.x.x5.x.x, and more.​

His mentor warned him:

“Updates are great… but they can also break your app if you’re not careful.”

This is where Semantic Versioning (SemVer) comes in.​


Semantic versioning: MAJOR.MINOR.PATCH in real life

Every npm package has a version:

MAJOR.MINOR.PATCH

For example: 4.17.1.

  • MAJOR – big, breaking changes.

  • MINOR – new features, backward compatible.

  • PATCH – bug fixes, no breaking changes.​

Examples:

  • 1.0.0 → first stable release.

  • Next patch: 1.0.1.

  • Next minor: 1.1.0.

  • Next major: 2.0.0.

Importantly, these numbers don’t behave like decimals:

  • After 1.9.15, the next minor might be 1.10.0.

  • You might see a version like 3.28.45.

How npm uses these numbers in package.json

When Arjun installed chalkpackage.json recorded:

"chalk": "^5.2.0"

The ^ (caret) means:

  • npm can auto-update to newer MINOR or PATCH versions of 5.x.x (e.g., 5.3.05.2.1).

  • npm will not auto-update to 6.0.0.​

If it had been:

"chalk": "~5.2.0"

The ~ (tilde) would mean:

  • Allow only PATCH updates within 5.2.x (e.g., 5.2.15.2.2).

If there were no symbol:

"chalk": "5.2.0"

  • npm would stick to exactly 5.2.0 unless Arjun changed it.

Arjun’s safe workflow became:

  • For normal updates:

    • npm update → get safe MINOR/PATCH updates according to the ^ or ~ rules.​
  • For a major upgrade (e.g., 4.x.x → 5.x.x):

    • npm install chalk@5 → then test the app carefully.​

He understood that SemVer is not just theory; it’s what keeps his app from randomly breaking when packages evolve.


package-lock.json: Arjun’s time machine

A few days later, a teammate cloned Arjun’s repo and ran:

npm install npm run dev

Everything worked the same way. The reason? package-lock.json.​

Here’s the subtle problem it solves:

  • package.json allows version ranges like "chalk": "^5.2.0".

  • Arjun installed when the latest was 5.2.0.

  • His teammate might install when the latest is 5.3.1.

  • If 5.3.1 or one of its dependencies behaves slightly differently, the teammate might get bugs that Arjun doesn’t see.​

package-lock.json fixes this by:

  • Recording exact versions of every dependency and sub-dependency (MAJOR, MINOR, PATCH).​

  • For example, it might pin:

    • chalk at 5.2.0
    • ansi-styles at 6.1.0
    • supports-color at 7.2.0, etc.

When npm install sees package-lock.json:

  • It uses this file to recreate the exact same dependency tree that Arjun had.​

Arjun learned two best practices:

  • Commit package.json and package-lock.json to Git.​

  • Do not commit node_modules (too large and regenerable).

With this, “it works on my machine” bugs were much less likely.


The complete story: how npm fits Arjun’s daily life

By the end of the sprint, Arjun’s typical workflow looked like this:

  1. Start a project

    mkdir team-task-dashboard cd team-task-dashboard npm init -y

  2. Install runtime dependencies

    npm install express chalk

  3. Install dev dependencies

    npm install --save-dev nodemon

  4. Write scripts in package.json

    "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js"
    }

  5. Build features using packages

-   Use `express` to create `/tasks` API endpoints.

-   Use `chalk` to highlight important logs.

-   Use `nodemon` so changes reflect instantly.​
Enter fullscreen mode Exit fullscreen mode
  1. Share the project
-   Commit code + `package.json` + `package-lock.json`.​

-   Teammates run:

    `npm install npm run dev`

    and get the exact same environment.
Enter fullscreen mode Exit fullscreen mode
  1. Update dependencies safely
-   Occasionally run `npm outdated` and `npm update`.[](https://dev.to/sudiip__17/-npm-modules-explained-in-nodejs-a-beginner-to-intermediate-guide-e70)​

-   For major versions, update explicitly with `npm install package@version` and test.
Enter fullscreen mode Exit fullscreen mode

Arjun no longer thought of npm as “just a tool” – it was the story behind how his project came alive: how packages got installed, how they were versioned, how his logs turned green and red, and how his team all ran the same code in the same way.


Top comments (0)