ποΈ Package.json & Dependency Management
If package.json feels like a formality npm generates and forgets, this lesson will change your mind. It is your project's rΓ©sumΓ©, recipe book, and instruction manual rolled into one file: it declares who you depend on, how those dependencies may update, and every task your team can run. Master it and you master the shape of every Node project you'll ever open.
Week 3 · Day 2 (Tuesday: NPM and Package Management) · Lecture 2
π― Learning Objectives
By the end of this lesson, you will be able to:
- Identify each metadata field in
package.jsonand write valid values for it - Distinguish the four dependency types and place a package in the correct one
- Explain what
peerDependenciessolve and when a library should declare them - Choose an appropriate version constraint (
^,~, exact) for a given dependency - Configure lifecycle and custom scripts, including cross-platform patterns
- Diagnose and resolve a dependency version conflict using
overridesandnpm ls
Estimated Time: 75 minutes
Practice: Author a complete package.json for a full-stack app and resolve a version clash.
In This Lesson
Anatomy of package.json
Every field in package.json serves one of four broad purposes: describing the project (metadata), listing what it needs (dependencies), automating tasks (scripts), and controlling how it builds and publishes (configuration). Keeping this mental map handy makes even an unfamiliar manifest readable at a glance.
Metadata Fields
name and version β the identity
Together, name and version uniquely identify your package on the registry. The name has strict rules; the version must be valid SemVer.
{
"name": "my-awesome-project",
"version": "1.2.3"
}
Rule for name | Valid | Invalid |
|---|---|---|
| Lowercase only | my-package | My Package |
| No spaces; hyphens/underscores OK | react-carousel | react carousel |
| No file extension | date-utils | date-utils.js |
| May be scoped | @mycompany/utils | node_modules (reserved) |
description and keywords β discoverability
{
"description": "A utility library for formatting dates and times",
"keywords": ["date", "time", "format", "datetime", "utility"]
}
Keep the description concrete β say what problem the package solves, not how great it is. Keywords feed npm's search index, so choose terms a user would actually type.
author, license, and repository
{
"author": "Jane Doe <jane@example.com> (https://janedoe.com)",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/username/repo.git"
},
"bugs": { "url": "https://github.com/username/repo/issues" },
"homepage": "https://project-website.com"
}
π‘ Which license?
MIT and ISC are permissive and allow commercial use β the common choice for open source. Apache-2.0 adds an explicit patent grant. GPL-3.0 is copyleft: derivative works must also be open source. Use the SPDX identifier string (e.g. "MIT"), or "UNLICENSED" for proprietary code you never intend to publish.
The Four Dependency Types
npm sorts your dependencies into four buckets, and the bucket decides when a package gets installed. Choosing correctly keeps production installs small and prevents duplicate-copy bugs.
dependencies & devDependencies
{
"dependencies": {
"express": "^4.18.2",
"lodash": "~4.17.21"
},
"devDependencies": {
"jest": "^29.5.0",
"eslint": "^8.38.0",
"webpack": "^5.80.0"
}
}
The distinction becomes concrete when someone else installs your package as a dependency: npm pulls in your dependencies but skips your devDependencies. Your test framework shouldn't ship to their production server.
peerDependencies β "you bring your own React"
A plugin like a React component library needs React to function, but it must use the same React instance as the host app β two copies of React break hooks. So instead of listing React as a dependency, the library declares it as a peer: "I need React 16.8+, and I expect you to provide it."
{
"name": "my-react-components",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"react-dom": { "optional": true }
}
}
optionalDependencies β install may fail, and that's OK
Some packages are platform-specific (macOS file-watching, native compilers). Listing them as optional means a failed install won't abort everything β but your code must handle their absence:
// Fall back gracefully if the optional native module isn't present
let sass;
try {
sass = require('node-sass'); // fast native binding
} catch (e) {
console.warn('node-sass unavailable β using pure-JS fallback');
sass = require('sass'); // slower, but always installs
}
Version Constraints
You met SemVer ranges in the last lesson. Here's the full toolbox, so you can pick the right amount of freedom for each dependency:
// Exact β no updates at all
"lodash": "4.17.21"
// Caret β minor + patch, lock major (>=4.17.21 <5.0.0)
"lodash": "^4.17.21"
// Tilde β patch only, lock minor (>=4.17.21 <4.18.0)
"lodash": "~4.17.21"
// Comparators and hyphen ranges
"lodash": ">=4.17.0 <4.18.0"
"lodash": "4.17.0 - 4.17.21"
// Wildcards
"lodash": "4.x" // >=4.0.0 <5.0.0
"lodash": "4.17.x" // >=4.17.0 <4.18.0
// OR multiple ranges
"lodash": "^4.17.0 || ^5.0.0"
// From Git or a local path
"lib": "github:owner/lib#4.17.21"
"my-module": "file:../my-module"
How npm resolves a range
Package A requires lodash@^4.17.0
Package B requires lodash@^4.17.15
β npm installs lodash@4.17.21 (newest version satisfying BOTH)
Package A requires react@^17.0.0
Package B requires react@^18.0.0
β No single version satisfies both β peer dependency warning/error
β A sensible default policy
Use ^ for well-maintained libraries you trust to honor SemVer. Tighten to ~ for anything where a surprise behavior change would hurt. Pin an exact version for truly critical, hard-to-test dependencies (a database driver, a payment SDK). Avoid "*" and "latest" in committed code β they make builds non-reproducible.
Scripts Configuration
Lifecycle scripts run automatically
Certain script names are hooks npm fires at specific moments β you don't call them directly. The most useful are the pre/post pairs and the publish hooks:
{
"scripts": {
"prepare": "husky install", // after install & before publish
"prepublishOnly": "npm test && npm run build", // only on npm publish
"pretest": "npm run lint", // before "npm test"
"test": "jest",
"posttest": "npm run cleanup" // after "npm test"
}
}
Custom scripts are your project's control panel
{
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "jest",
"test:coverage": "jest --coverage",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"build": "webpack --mode production",
"clean": "rimraf dist",
"prebuild": "npm run clean",
"format": "prettier --write \"src/**/*.{js,jsx,json,css}\"",
"validate": "npm run lint && npm test && npm run build"
}
}
Note the naming convention: a colon (test:coverage) groups related variants, and a validate-style script chains several checks into one gate you can run before every commit.
Cross-platform scripts
A script that works on your Mac may fail on a teammate's Windows machine. Environment-variable syntax and file deletion differ between shells. Small helper packages smooth this over:
{
"scripts": {
"dev": "cross-env NODE_ENV=development node server.js",
"clean": "rimraf dist",
"build": "run-s clean compile",
"compile": "tsc"
},
"devDependencies": {
"cross-env": "^7.0.3", // set env vars the same way everywhere
"rimraf": "^5.0.0", // cross-platform "rm -rf"
"npm-run-all": "^4.1.5" // run-s / run-p for sequential/parallel
}
}
β οΈ NODE_ENV=production npm start isn't portable
Prefixing a variable inline works in bash but throws on Windows cmd. Use cross-env in your scripts so every contributor β and every CI runner β behaves identically.
Advanced Configuration
Engine constraints
{
"engines": {
"node": ">=18.0.0",
"npm": ">=9.0.0"
}
}
By default engines is advisory β npm only warns. Enforce it with an .npmrc containing engine-strict=true, which turns a mismatch into a hard error.
Entry points and the files whitelist
{
"main": "dist/index.js", // CommonJS entry point
"module": "dist/index.esm.js", // ES module entry point
"types": "dist/index.d.ts", // TypeScript type definitions
"exports": {
".": {
"import": "./dist/index.esm.js",
"require": "./dist/index.js"
}
},
"bin": { "my-cli": "./cli.js" }, // installs an executable command
"files": ["dist/", "README.md"] // ONLY these get published
}
The files array is a publish whitelist: only the listed paths (plus package.json, README, and LICENSE, which are always included) go into the tarball. It's the cleanest way to keep your source, tests, and configs out of the published package.
Guarding against accidental publish
{
"private": true, // npm refuses to publish this package
"publishConfig": {
"access": "public", // publish scoped packages publicly
"registry": "https://registry.npmjs.org/"
}
}
π‘ "private": true is cheap insurance
Set it on any app or internal repo you never mean to publish. It makes an accidental npm publish fail loudly instead of leaking your code to the public registry.
Resolving Conflicts
When two of your dependencies each need an incompatible version of a third package, npm normally installs both copies nested inside node_modules. That's fine for most libraries β but for singletons (React) or when you need to force a security patch, you have to intervene.
The tools
# See which versions of a package are installed and WHY
npm ls lodash # every copy in the tree
npm explain lodash # the dependency chain that pulled it in
# Try to flatten duplicate copies
npm dedupe
When you must force a version everywhere, npm's overrides (npm 8.3+) is the sanctioned tool:
{
"overrides": {
"lodash": "^4.17.21", // force this version project-wide
"package-a": {
"lodash": "^4.17.21" // β¦or only inside package-a's subtree
}
}
}
β οΈ Overrides are a last resort
Forcing a version can break a dependency that genuinely relied on the old one. Prefer updating the outdated package first; reach for overrides only to patch a vulnerability you can't otherwise clear, and test thoroughly afterward. (Yarn's equivalent is resolutions.)
Practice & Quiz
ποΈ Exercise 1: Author a full-stack package.json
Goal: Write a manifest for a MERN-style app. It should have Express and Mongoose as runtime deps, Nodemon and Jest as dev deps, a dev script using nodemon, a test script, and a Node engine constraint of 18+.
π‘ Hint
Runtime deps (Express, Mongoose) go in dependencies; tooling (Nodemon, Jest) in devDependencies. The engine goes in an engines object.
β Solution
{
"name": "fullstack-app",
"version": "1.0.0",
"description": "A full-stack JavaScript application",
"main": "server/index.js",
"scripts": {
"start": "node server/index.js",
"dev": "nodemon server/index.js",
"test": "jest",
"lint": "eslint ."
},
"dependencies": {
"express": "^4.18.2",
"mongoose": "^7.0.3",
"cors": "^2.8.5",
"dotenv": "^16.0.3"
},
"devDependencies": {
"nodemon": "^3.0.1",
"jest": "^29.5.0",
"eslint": "^8.38.0"
},
"engines": { "node": ">=18.0.0" },
"license": "MIT"
}
ποΈ Exercise 2: Resolve a React clash
Goal: Your app pins react@18.2.0. Library A declares a peer dependency on react@^17.0.0, Library B on react@^18.0.0. Diagnose the conflict and propose the cleanest fix.
β Solution
Run npm ls react to confirm the clash β Library A's peer range (^17) excludes your 18.2.0, so npm emits a peer-dependency warning.
Best fix: upgrade Library A to a version that supports React 18 (check its releases). If none exists, you can quiet the warning with --legacy-peer-deps or an overrides entry β but only after verifying Library A actually works under React 18, since its author hasn't promised it does.
π― Quick Quiz
Question 1: A React component library should list react as aβ¦
Question 2: What does the "files" array control?
Question 3: Which command explains why a particular package is installed?
Best Practices & Pitfalls
β Do
- Put every package in the correct dependency bucket β production stays lean
- Declare shared singletons (React, a plugin's host) as
peerDependencies - Use a
fileswhitelist so you publish only build output, not source - Set
"private": trueon any repo you never intend to publish - Add an
enginesfield so contributors know the required Node version
β Don't
- Use
"*"or"latest"as a version β builds become unpredictable - Reach for
overridesbefore trying to update the offending package - Write inline
VAR=valuein scripts withoutcross-envβ it breaks on Windows - Hand-edit the lockfile to "fix" a conflict β resolve it in
package.jsonand reinstall - Dump test and build tools into
dependenciesβ they belong indevDependencies
Summary
π Key Takeaways
package.jsonorganizes into metadata, dependencies, scripts, and configuration- There are four dependency types; the bucket decides when β and whether β a package installs
peerDependenciesask the host app to supply a shared package, preventing duplicate copies- Version constraints trade freedom for stability:
^>~> exact - Resolve conflicts with
npm ls/npm explainfirst, andoverridesonly as a last resort
π Additional Resources
- npm Docs β package.json reference
- npm Docs β package-lock.json
- npm Docs β overrides
- nodejs.org β Modules: Packages (exports, main, type)
π What's Next?
You can now read and author any manifest. In the next lesson you flip roles β from consumer to author β and build, document, test, and ship your own package in Creating and Publishing Packages.
π Manifest mastered!
The file that used to look like boilerplate is now a map you can read fluently β for any project on the planet.