Episode 3 — NodeJS MongoDB Backend Architecture / 3.1 — Starting with NodeJS

3.1.d — NPM Basics

In one sentence: npm (Node Package Manager) is the world's largest software registry and the CLI tool that lets you install, manage, and share reusable JavaScript packages for your Node.js projects.


Navigation: ← 3.1 Overview · Next → 3.1.e Package JSON Deep Dive


Table of Contents


1. What Is npm?

npm is actually three things:

┌─────────────────────────────────────────────────────────────────┐
│                      npm IS THREE THINGS                        │
│                                                                 │
│  1. THE REGISTRY (npmjs.com)                                   │
│     └── The world's largest software registry                  │
│     └── 2M+ packages, 30B+ downloads/week                     │
│     └── Anyone can publish a package for free                  │
│                                                                 │
│  2. THE CLI TOOL (npm command)                                 │
│     └── Comes bundled with Node.js                             │
│     └── Install, update, remove, publish packages              │
│     └── Manage project dependencies and scripts                │
│                                                                 │
│  3. THE WEBSITE (npmjs.com)                                    │
│     └── Search for packages, read docs                         │
│     └── Manage your published packages                         │
│     └── View download stats and security advisories            │
└─────────────────────────────────────────────────────────────────┘

What is a "package"?

A package is a reusable bundle of code that solves a specific problem. Instead of writing everything from scratch, you install packages that other developers have already built and tested.

// WITHOUT a package — writing your own date formatter
function formatDate(date) {
  const months = ['Jan', 'Feb', 'Mar', /* ... 9 more */];
  const d = new Date(date);
  return `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
  // What about timezones? Locales? Relative time? Edge cases?
  // This quickly becomes 100+ lines of code with bugs.
}

// WITH a package — one line
const dayjs = require('dayjs');
console.log(dayjs().format('MMM D, YYYY'));         // Apr 11, 2026
console.log(dayjs('2009-05-27').fromNow());         // 17 years ago

2. npm vs yarn vs pnpm

Featurenpmyarn (Classic/Berry)pnpm
Created byIsaac Z. Schlueter (2010)Facebook (2016)Zoltan Kochan (2017)
Comes with Node.jsYesNo (install separately)No (install separately)
SpeedGood (improved greatly)Fast (parallel installs)Fastest (hard links, symlinks)
Disk usageOne copy per projectOne copy per projectShared store (saves disk space)
Lock filepackage-lock.jsonyarn.lockpnpm-lock.yaml
WorkspacesYes (npm 7+)YesYes
Securitynpm audityarn auditpnpm audit
Market shareDominant (default)SignificantGrowing fast

Which should you use?

Starting out?           → Use npm (it's already installed)
Working on a team?      → Use whatever the team uses (check the lock file)
Performance matters?    → Consider pnpm (fastest, most disk-efficient)

For this course, we use npm. The concepts (install, dependencies, scripts) are the same across all three.


3. Initializing a Project — npm init

Every Node.js project needs a package.json file. It is the identity card of your project.

Interactive initialization

npm init
# It will ask you questions:
# package name: (my-project)
# version: (1.0.0)
# description: A sample Node.js project
# entry point: (index.js)
# test command: jest
# git repository:
# keywords: node, learning
# author: Arjun
# license: (ISC) MIT
#
# Creates package.json with your answers

Quick initialization (skip questions)

npm init -y
# Creates package.json with ALL defaults — no questions asked
{
  "name": "my-project",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Setting defaults for npm init

# Set your defaults once — used every time you run npm init
npm config set init-author-name "Arjun Sharma"
npm config set init-author-email "arjun@example.com"
npm config set init-license "MIT"
npm config set init-version "0.1.0"

4. Installing Packages

Installing a production dependency

# Full syntax
npm install express

# Shorthand
npm i express

# Install a specific version
npm i express@4.18.2

# Install multiple packages at once
npm i express mongoose dotenv

What happens when you run npm install express:

1. npm queries the registry (registry.npmjs.org) for "express"
2. Downloads express and ALL its sub-dependencies
3. Places everything in ./node_modules/
4. Adds "express" to "dependencies" in package.json
5. Records exact versions in package-lock.json

Install flags

# Production dependency (default — goes into "dependencies")
npm install express
npm i express
npm i express --save          # Explicit (same as default since npm 5)

# Development dependency (goes into "devDependencies")
npm install nodemon --save-dev
npm i nodemon -D              # Shorthand for --save-dev

# Global installation (available system-wide, not project-specific)
npm install -g nodemon
npm i -g typescript

# Install exact version (no ^ or ~ in package.json)
npm install express --save-exact
npm i express -E

# Install from a Git repository
npm install git+https://github.com/user/repo.git

# Install all dependencies from package.json (in a cloned project)
npm install
npm i

Install flags summary

FlagShortWhere It SavesUse Case
(none) / --savedependenciesPackages needed at runtime (express, mongoose)
--save-dev-DdevDependenciesPackages for development only (nodemon, jest, eslint)
--global-gSystem-wideCLI tools you want everywhere (typescript, npm-check-updates)
--save-exact-EExact versionWhen you need a pinned version
--no-saveNot savedOne-off install, not added to package.json

5. Dependencies vs devDependencies

This distinction matters in production deployments.

{
  "dependencies": {
    "express": "^4.18.2",
    "mongoose": "^7.6.3",
    "dotenv": "^16.3.1",
    "bcrypt": "^5.1.1",
    "jsonwebtoken": "^9.0.2"
  },
  "devDependencies": {
    "nodemon": "^3.0.2",
    "jest": "^29.7.0",
    "eslint": "^8.56.0",
    "prettier": "^3.1.1",
    "supertest": "^6.3.3"
  }
}

When to use which

TypeWhen to UseExamples
dependenciesCode that runs in production — your app needs it to workexpress, mongoose, bcrypt, jsonwebtoken, cors
devDependenciesCode only needed during development/testing/building — NOT in productionnodemon, jest, eslint, prettier, typescript, webpack

Why it matters

# In production, you can skip devDependencies to save space/time:
npm install --production
# OR
NODE_ENV=production npm install

# This skips nodemon, jest, eslint, etc. — only installs what the app needs
# Result: smaller node_modules, faster deploys, less attack surface

Quick decision guide

Ask yourself: "Does my running application need this package to work?"

YES → dependencies
  express (handles HTTP requests)
  mongoose (talks to database)
  bcrypt (hashes passwords at runtime)

NO → devDependencies
  nodemon (restarts server during dev — not needed in production)
  jest (runs tests — tests don't run in production)
  eslint (checks code style — already checked before deploy)

6. The node_modules Folder

What is it?

When you run npm install, all packages (and their sub-dependencies) are downloaded into the node_modules directory.

my-project/
├── node_modules/           ← All installed packages live here
│   ├── express/            ← The express package
│   │   ├── lib/
│   │   ├── index.js
│   │   └── package.json
│   ├── accepts/            ← A dependency OF express
│   ├── body-parser/        ← Another dependency of express
│   ├── cookie/
│   ├── ... (30+ packages just for express)
│   └── .package-lock.json
├── package.json
└── package-lock.json

Why is node_modules so large?

# Install express (one package)
npm i express

# Check how many packages were installed
ls node_modules | wc -l
# ~65 packages!

# Check the size
du -sh node_modules
# ~8 MB for just express
# A real project can easily have 200-500 MB in node_modules

Express depends on other packages, which depend on other packages, which depend on other packages. This is the dependency tree.

The golden rule: NEVER commit node_modules

┌─────────────────────────────────────────────────────────────────┐
│                     CRITICAL RULE                                │
│                                                                 │
│  NEVER commit node_modules to Git.                             │
│                                                                 │
│  Why:                                                          │
│  • It's huge (100s of MB — bloats your repository)             │
│  • It's reproducible (npm install recreates it from lock file) │
│  • It contains platform-specific binaries (won't work cross-OS)│
│  • It changes constantly (noise in git history)                │
│                                                                 │
│  Instead: Commit package.json + package-lock.json              │
│  Other developers run: npm install                             │
└─────────────────────────────────────────────────────────────────┘

.gitignore setup

# Create .gitignore with essential entries
echo "# Dependencies
node_modules/

# Environment variables
.env
.env.local
.env.*.local

# OS files
.DS_Store
Thumbs.db

# Build output
dist/
build/

# Test coverage
coverage/

# Editor directories
.vscode/
.idea/" > .gitignore

Recreating node_modules

# Scenario: You clone a project from GitHub
git clone https://github.com/user/project.git
cd project

# node_modules doesn't exist yet — that's expected!
npm install
# npm reads package.json and package-lock.json
# Downloads and installs everything into node_modules/
# Now you're ready to go

7. npx — Run Without Installing

npx comes bundled with npm (5.2+). It runs packages without installing them globally.

Why npx exists

# WITHOUT npx — you had to install globally first
npm install -g create-react-app
create-react-app my-app
# Problem: The global package gets outdated; takes up space

# WITH npx — always uses the latest version, no global install
npx create-react-app my-app
# Downloads temporarily, runs, then cleans up

Common npx use cases

# Create a React app
npx create-react-app my-app

# Create a Next.js app
npx create-next-app@latest my-app

# Create an Express app with generator
npx express-generator my-api

# Run a locally installed package
npx nodemon server.js
# (Finds nodemon in ./node_modules/.bin/ and runs it)

# Initialize ESLint config
npx eslint --init

# Run a specific version of a package
npx node@18 -e "console.log(process.version)"

# Execute a GitHub gist
npx https://gist.github.com/user/abc123

npx vs npm

Scenarionpmnpx
Install a package permanentlynpm i -g typescriptN/A
Run a package once without installingN/Anpx typescript --version
Run a locally installed tool./node_modules/.bin/jestnpx jest
Use the latest version of a generatorInstall, then runnpx create-next-app@latest

8. Semantic Versioning (SemVer)

Every npm package uses semantic versioning — a three-number system that communicates what changed.

The format: MAJOR.MINOR.PATCH

    4   .   18  .   2
    │        │       │
    │        │       └── PATCH: Bug fixes, no new features
    │        │            (safe to update — nothing breaks)
    │        │
    │        └── MINOR: New features, backward-compatible
    │             (safe to update — old code still works)
    │
    └── MAJOR: Breaking changes
         (DANGEROUS to update — old code may break!)

Real-world examples

express 4.18.2 → 4.18.3    PATCH: Fixed a security bug
express 4.18.2 → 4.19.0    MINOR: Added new middleware feature
express 4.18.2 → 5.0.0     MAJOR: Changed API — your code might break!

Version ranges in package.json

SymbolMeaningExampleMatches
^ (caret)Compatible with version — allows MINOR + PATCH updates^4.18.2>=4.18.2 and <5.0.0
~ (tilde)Approximately — allows only PATCH updates~4.18.2>=4.18.2 and <4.19.0
(none)Exact version — no updates4.18.2Only 4.18.2
*Any version*Latest
>=Greater than or equal>=4.0.04.0.0 and above
4.xAny minor/patch in major 44.x>=4.0.0 and <5.0.0

The most common: ^ (caret)

{
  "dependencies": {
    "express": "^4.18.2"
  }
}

This means: "Install 4.18.2 or any newer version that's less than 5.0.0."

^4.18.2 allows:  4.18.3, 4.18.4, 4.19.0, 4.20.0, 4.99.99
^4.18.2 blocks:  5.0.0, 3.x.x

Why ^ is the default

  • MINOR and PATCH updates are supposed to be backward-compatible
  • You get bug fixes and new features automatically
  • You are protected from breaking changes (major version bump)

9. Managing Packages — Update, Remove, Audit

Removing packages

# Remove from dependencies
npm uninstall express
npm un express            # Shorthand

# Remove from devDependencies
npm uninstall nodemon -D

# Remove a global package
npm uninstall -g typescript

Updating packages

# Check which packages are outdated
npm outdated
# Package    Current  Wanted  Latest  Location
# express     4.18.2  4.19.1  4.19.1  my-project
# mongoose    7.6.3   7.8.0   8.1.0   my-project
#                              ↑
#                    Latest may be a MAJOR update (dangerous)

# Update all packages (respects semver ranges in package.json)
npm update

# Update a specific package
npm update express

# Update to the absolute latest (including major — be careful!)
npm install express@latest

# Interactive update tool (third-party, very useful)
npx npm-check-updates -i

Listing installed packages

# List top-level dependencies
npm ls --depth=0
# my-project@1.0.0
# ├── express@4.18.2
# ├── mongoose@7.6.3
# └── nodemon@3.0.2

# List all dependencies (including sub-dependencies)
npm ls

# List globally installed packages
npm ls -g --depth=0

10. npm Scripts

npm scripts let you define shortcuts for common commands in your package.json.

Defining scripts

{
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js",
    "test": "jest",
    "test:watch": "jest --watch",
    "lint": "eslint src/",
    "lint:fix": "eslint src/ --fix",
    "format": "prettier --write src/",
    "build": "tsc",
    "clean": "rm -rf dist node_modules",
    "seed": "node scripts/seed-database.js",
    "migrate": "node scripts/migrate.js"
  }
}

Running scripts

# Special scripts (don't need "run")
npm start                  # Runs "start" script
npm test                   # Runs "test" script
npm stop                   # Runs "stop" script
npm restart                # Runs "restart" (or stop + start)

# Custom scripts (need "run")
npm run dev                # Runs "dev" script
npm run lint               # Runs "lint" script
npm run build              # Runs "build" script
npm run seed               # Runs "seed" script

# List all available scripts
npm run
# Lifecycle scripts included in my-project:
#   start: node server.js
#   test: jest
# available via `npm run-script`:
#   dev: nodemon server.js
#   lint: eslint src/
#   ...

Pre and post hooks

npm automatically runs pre and post scripts if they exist:

{
  "scripts": {
    "prebuild": "echo 'Cleaning old build...' && rm -rf dist",
    "build": "tsc",
    "postbuild": "echo 'Build complete!'",

    "pretest": "npm run lint",
    "test": "jest",
    "posttest": "echo 'All tests passed!'"
  }
}
npm run build
# Runs in order:
# 1. prebuild  → Cleaning old build...
# 2. build     → tsc (TypeScript compiler)
# 3. postbuild → Build complete!

Common script patterns

ScriptCommandPurpose
startnode server.jsStart the production server
devnodemon server.js or node --watch server.jsStart with auto-restart
testjest or mochaRun test suite
test:watchjest --watchRun tests on file change
linteslint .Check code for errors
lint:fixeslint . --fixAuto-fix linting errors
formatprettier --write .Format all code
buildtsc or webpackCompile/bundle for production
cleanrm -rf dist node_modulesRemove generated files
seednode scripts/seed.jsPopulate database with test data

Passing arguments to scripts

# Pass arguments after --
npm run test -- --verbose --coverage
# Equivalent to: jest --verbose --coverage

npm run dev -- --port 4000
# Equivalent to: nodemon server.js --port 4000

11. Security — npm audit

Why security matters

When you install a package, you are running someone else's code on your machine. That code can have vulnerabilities.

Running an audit

npm audit
# ┌───────────────┬──────────────────────────────────────────────────────┐
# │ Severity      │ high                                                 │
# ├───────────────┼──────────────────────────────────────────────────────┤
# │ Package       │ express                                              │
# ├───────────────┼──────────────────────────────────────────────────────┤
# │ Dependency of │ express                                              │
# ├───────────────┼──────────────────────────────────────────────────────┤
# │ Path          │ express > qs                                         │
# ├───────────────┼──────────────────────────────────────────────────────┤
# │ More info     │ https://npmjs.com/advisories/1234                    │
# └───────────────┴──────────────────────────────────────────────────────┘
# found 2 vulnerabilities (1 moderate, 1 high)

# Auto-fix vulnerabilities (safe — only updates within semver range)
npm audit fix

# Force fix (may include breaking changes — use carefully)
npm audit fix --force

# See what would change without actually doing it
npm audit fix --dry-run

# Get a detailed JSON report
npm audit --json

Security best practices

PracticeWhy
Run npm audit regularlyCatches known vulnerabilities early
Keep dependencies updatedOlder packages have more known vulnerabilities
Use package-lock.jsonLocks exact versions — prevents unexpected updates
Review before installingCheck download counts, maintenance status, last update on npmjs.com
Avoid unused packagesEvery dependency is an attack surface — remove what you don't use
Use npx instead of -gGlobal packages are harder to track and update
Check npm ls for duplicatesDuplicate packages increase risk and bundle size

Key Takeaways

  1. npm is three things: a registry (2M+ packages), a CLI tool (comes with Node.js), and a website (npmjs.com).
  2. npm init -y creates a package.json instantly — the starting point of every Node.js project.
  3. npm install <package> downloads a package and all its sub-dependencies into node_modules/.
  4. dependencies are for runtime code (express, mongoose); devDependencies are for development-only tools (nodemon, jest).
  5. Never commit node_modules — add it to .gitignore and let npm install recreate it from the lock file.
  6. npx runs packages without global installation — always up to date, no cleanup needed.
  7. Semantic versioning (^4.18.2) uses MAJOR.MINOR.PATCH — the ^ allows safe minor and patch updates.
  8. npm scripts in package.json are the standard way to define project commands — npm start, npm test, npm run dev.
  9. npm audit checks for known security vulnerabilities — run it regularly and fix issues promptly.

Explain-It Challenge

Can you explain to a friend: "What happens when you run npm install express? Where does the code go, how does package.json change, and why should you never commit node_modules?" If you can do it in under 90 seconds without notes, you have mastered this topic.


Navigation: ← 3.1 Overview · Next → 3.1.e Package JSON Deep Dive