Back/Module F-5 npm and the Node.js Ecosystem
Module F-5·18 min read

package.json anatomy, semantic versioning, package-lock.json, npm scripts, and the essential dev tools every Node.js engineer installs first.

Module F-5 — npm and the Node.js Ecosystem

What this module covers: npm is the package manager for Node.js — but it is also the command-line tool you use to run scripts, manage dependencies, and maintain your project. Understanding package.json, semantic versioning, the lock file, and the npm CLI is not optional. Every Node.js project uses these. This module also covers the essential packages every engineer installs on day one and the most common mistakes that cost hours to debug.


What npm Is

npm stands for Node Package Manager. It ships with Node.js — when you install Node, you get npm automatically. It does three things:

  1. Registry: A central database of over two million open-source JavaScript packages at npmjs.com
  2. CLI: The npm command you run to install, update, and remove packages
  3. Package format: The conventions (package.json, node_modules, lock files) that define a Node.js project

When you run npm install express, npm downloads the Express package and its dependencies from the registry into your project's node_modules folder.


package.json

Every Node.js project has a package.json at its root. It is the project's manifest — its name, version, dependencies, scripts, and configuration. Create one:

bash
npm init # Interactive prompts npm init -y # Accept all defaults (fastest)

A typical package.json:

json
{ "name": "my-api", "version": "1.0.0", "description": "A simple REST API", "main": "src/index.js", "type": "module", "scripts": { "start": "node src/index.js", "dev": "nodemon src/index.js", "test": "jest", "lint": "eslint src/" }, "dependencies": { "express": "^4.18.2", "dotenv": "^16.3.1" }, "devDependencies": { "nodemon": "^3.0.1", "jest": "^29.7.0", "eslint": "^8.55.0" }, "engines": { "node": ">=20.0.0" } }

Key fields:

  • "name" — must be lowercase, no spaces. Used as the package name if you publish to npm.
  • "version" — follows semantic versioning (covered shortly).
  • "main" — the entry point when someone requires your package. For apps, this is your server entry file.
  • "type": "module" — makes all .js files use ES Modules. Omit for CommonJS.
  • "scripts" — shortcuts for commands. npm run dev runs whatever is under "dev".
  • "dependencies" — packages required to run in production.
  • "devDependencies" — packages only needed during development (testing, linting, build tools).
  • "engines" — declares the minimum Node.js version your project requires.

dependencies vs devDependencies

bash
npm install express # → goes in "dependencies" npm install --save-dev jest # → goes in "devDependencies" npm install -D nodemon # → shorthand for --save-dev

When you deploy to production, you can run npm install --omit=dev to skip devDependencies — keeping the production image smaller.


Semantic Versioning

Every npm package uses semantic versioning (semver): MAJOR.MINOR.PATCH.

PartWhen it changesExample
MAJORBreaking change — your code may need updating4.0.05.0.0
MINORNew features, backwards compatible4.18.04.19.0
PATCHBug fixes, backwards compatible4.18.24.18.3

In package.json, version ranges control which updates you accept automatically:

json
"express": "4.18.2" // exact — only this version "express": "~4.18.2" // patch updates only — 4.18.x "express": "^4.18.2" // minor + patch updates — 4.x.x (most common) "express": "*" // any version — avoid this "express": ">=4.0.0" // 4.0.0 or higher

^ (caret) is the default when you npm install express. It allows 4.19.0, 4.18.3, but not 5.0.0.

The practical rule: use ^ for most packages. Pin to an exact version only if the package is known to have breaking changes in minor versions (rare but it happens).


package-lock.json

The lock file records the exact version of every package — and every package's dependencies — that was installed. This is critical.

The problem it solves: package.json says "express": "^4.18.2". On Monday you install and get 4.18.2. On Tuesday a colleague installs and gets 4.19.0 (just released). Your environments are different. The lock file ensures everyone gets exactly the same versions.

bash
# Always commit package-lock.json to version control git add package-lock.json
bash
# Install from lock file exactly (use in CI/CD and production) npm ci # Install and update lock file (use during development when adding packages) npm install

Key rule: npm ci for CI/CD pipelines and Docker builds. npm install when you're actively developing and want to add/update packages.

Never manually edit package-lock.json. It is generated by npm.


Essential npm Commands

bash
# Install all dependencies (reads package.json + lock file) npm install # Install a specific package npm install express npm install -D nodemon # devDependency # Install a specific version npm install express@4.18.2 npm install express@latest # latest published version # Remove a package npm uninstall express # Update packages (respects semver ranges in package.json) npm update npm update express # update specific package # Check for outdated packages npm outdated # See what's installed npm list npm list --depth=0 # only top-level packages # Run a script from package.json npm run dev npm run build npm test # shorthand for npm run test npm start # shorthand for npm run start # Check for security vulnerabilities npm audit npm audit fix # auto-fix safe updates

npx: Run Without Installing

npx runs a package without permanently installing it. Useful for one-off tools and project generators:

bash
# Create a new project (runs create-react-app without installing it globally) npx create-react-app my-app # Run a package that's not installed npx cowsay "Hello from Node.js" # Run a specific version npx node@20 --version # Run a local package binary (no npx needed — npm scripts handle this) npx jest # runs jest from node_modules/.bin/jest

npm Scripts

The "scripts" field in package.json is more powerful than it looks. npm adds node_modules/.bin to the PATH when running scripts, so you can reference package binaries directly without npx:

json
{ "scripts": { "start": "node src/index.js", "dev": "nodemon src/index.js --watch src", "build": "tsc --outDir dist", "test": "jest --coverage", "test:watch": "jest --watch", "lint": "eslint src --ext .ts,.js", "lint:fix": "eslint src --ext .ts,.js --fix", "db:migrate": "prisma migrate dev", "db:seed": "node scripts/seed.js", "clean": "rm -rf dist node_modules" } }

You can also run scripts in sequence (&&) or in parallel (&):

json
{ "scripts": { "build:all": "npm run lint && npm run test && npm run build", "dev:all": "node server.js & npx tailwindcss --watch" } }

Pre and post hooks run automatically:

json
{ "scripts": { "pretest": "npm run lint", // runs before npm test "test": "jest", "posttest": "npm run clean" // runs after npm test } }

node_modules

When you run npm install, every package and its dependencies are downloaded into node_modules/. This folder can be enormous — a typical project has hundreds of megabytes. Two rules:

  1. Never commit node_modules to git. Add it to .gitignore.
  2. Always commit package-lock.json to git. Your colleagues need it to get the same versions.
bash
# .gitignore node_modules/ dist/ .env *.log

If node_modules gets corrupted or out of sync, delete it and reinstall:

bash
rm -rf node_modules npm install

Essential Packages for Every Project

These are the packages you will install in almost every Node.js project:

Development tools:

bash
# Auto-restarts server when files change npm install -D nodemon # TypeScript (covered in P-3) npm install -D typescript ts-node @types/node # ESLint for code quality npm install -D eslint # Prettier for formatting npm install -D prettier

Runtime utilities:

bash
# Environment variables from .env file npm install dotenv # HTTP framework (covered in F-6) npm install express # TypeScript types for Express npm install -D @types/express # Schema validation (covered in P-4) npm install zod # HTTP client for outbound requests npm install axios # or use built-in fetch (Node 18+) — no install needed

Nodemon configuration — create nodemon.json at the project root to avoid typing flags:

json
{ "watch": ["src"], "ext": "ts,js,json", "ignore": ["src/**/*.test.ts"], "exec": "ts-node src/index.ts" }

Global vs Local Packages

Most packages should be installed locally (in your project's node_modules). This ensures every project uses its own version and colleagues get the same thing.

bash
# Local install (correct for project dependencies) npm install express npm install -D nodemon # Global install (use only for CLI tools you run anywhere) npm install -g typescript npm install -g nodemon # acceptable for personal tools # See globally installed packages npm list -g --depth=0

The rule: if the package is a dependency of your project, install it locally. If it's a CLI tool you want available everywhere on your machine (like ts-node for quick scripts), install globally. In CI/CD, always use local packages.


Workspaces (Monorepos)

If you have multiple packages in one repository (a monorepo), npm workspaces let you manage them together:

json
// Root package.json { "name": "my-monorepo", "workspaces": ["packages/*", "apps/*"] }
bash
npm install # installs dependencies for all workspaces npm run build -w api # run build in the 'api' workspace only

Workspaces are out of scope for Foundation — just know they exist for when you encounter them.


Common Mistakes

Installing in the wrong directory:

bash
# ❌ Wrong — running npm install from a subdirectory cd src && npm install express # ✅ Correct — always from the project root (where package.json is) npm install express

Committing .env:

bash
# Always in .gitignore .env .env.local .env.production

Not pinning the Node.js version:

json
// package.json — declare the minimum Node version your code requires { "engines": { "node": ">=20.0.0" } }

Also create a .nvmrc at the project root:

20

Then anyone running nvm use automatically switches to the right version.

Using npm install in CI/CD:

bash
# ❌ npm install can update the lock file npm install # ✅ npm ci installs exactly what's in the lock file and is faster npm ci

Summary

  • package.json is the project manifest. dependencies for runtime packages, devDependencies for dev tools. "scripts" for commands.
  • Semantic versioning: MAJOR.MINOR.PATCH. ^ (caret) allows minor and patch updates. Pin exact versions only when necessary.
  • package-lock.json ensures reproducible installs. Always commit it. Use npm ci in CI/CD.
  • npm install <pkg> for runtime, npm install -D <pkg> for dev tools. Never install project dependencies globally.
  • npx runs packages without installing them globally — ideal for project generators and one-off tools.
  • .nvmrc pins the Node.js version. .gitignore excludes node_modules/ and .env. Both are mandatory.

Next: building HTTP servers with Express — routing, middleware, query parameters, and the structure of a real REST API.

Discussion