Every time you run npm install, npm creates or updates two things: the node_modules directory and the package-lock.json file. Understanding what these contain, how they relate to each other, and why one must never be committed to Git while the other always should be โ is essential knowledge for working on any MERN project alone or in a team. Mishandling these files is the single most common source of “works on my machine” bugs in Node.js projects.
node_modules โ What Is Inside?
server/node_modules/
โโโ express/ โ Express itself
โ โโโ index.js
โ โโโ package.json
โโโ accepts/ โ dependency of Express (you did not install this)
โโโ body-parser/ โ dependency of Express
โโโ debug/ โ dependency of a dependency of Express
โโโ mongoose/ โ Mongoose itself
โ โโโ lib/
โ โโโ package.json
โโโ ... โ 100s of transitive dependencies
โโโ .bin/
โโโ nodemon โ CLI tools installed as executables
โโโ jest
Running "npm install express" actually installs:
express (1 package you asked for)
+ ~50 transitive dependencies (express's dependencies and their dependencies)
A typical MERN server project with ~10 direct dependencies
will have ~300-500 packages in node_modules totalling 50-150 MB
node_modules. Any changes you make will be overwritten the next time someone runs npm install. If you need to patch a package, use npm’s patch-package tool which records the diff and reapplies it automatically after install โ but this should be a last resort.node_modules and running npm install fresh. This cleans up any ghost packages from the previous branch. On Unix: rm -rf node_modules && npm install. For Windows PowerShell: Remove-Item -Recurse -Force node_modules; npm install.node_modules to Git. It contains hundreds of thousands of files, makes repositories gigabytes in size, makes git status and git log unusable, and provides zero benefit since anyone can regenerate it exactly from package-lock.json with npm install. If you have accidentally committed it, see the Common Mistakes section below for how to remove it from history.package-lock.json โ Reproducible Installs
While package.json specifies version ranges (e.g. ^4.18.2), package-lock.json records the exact version that was actually installed for every package โ including all transitive dependencies. This guarantees that npm install produces byte-for-byte identical node_modules on every machine and in every CI environment.
// package-lock.json โ a snapshot of exact resolved versions
{
"name": "mern-blog-server",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mern-blog-server",
"dependencies": { "express": "^4.18.2" }
},
"node_modules/express": {
"version": "4.18.3",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.3.tgz",
"integrity": "sha512-...",
"dependencies": {
"accepts": "~1.3.8",
"body-parser": "1.20.2"
}
},
"node_modules/body-parser": {
"version": "1.20.2"
}
}
}
npm install vs npm ci
| npm install | npm ci | |
|---|---|---|
| Use when | Development โ adding/updating packages | CI/CD pipelines, production deploys |
| Reads | package.json (may update lock file) | package-lock.json only (never changes it) |
| node_modules | Updates incrementally | Deletes and reinstalls from scratch |
| Fails if | package.json is invalid | lock file is missing or out of sync with package.json |
| Speed | Slower (resolves dependencies) | Faster (uses exact lock file versions) |
The Complete MERN .gitignore
# โโ Root .gitignore for a MERN monorepo โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# โโ Dependencies โ NEVER commit these โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
node_modules/
**/node_modules/
# โโ Environment variables โ contain secrets โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# โโ Build output โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
dist/
build/
client/dist/
# โโ Logs โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
logs/
*.log
npm-debug.log*
yarn-debug.log*
# โโ Testing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
coverage/
.nyc_output/
# โโ OS files โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
.DS_Store
Thumbs.db
desktop.ini
# โโ Editor files โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
.vscode/settings.json โ only if you do NOT want to share editor settings
# Note: .vscode/extensions.json and .vscode/launch.json are worth committing
*.swp
*.swo
# โโ Uploads directory (user-generated content) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
server/uploads/*
!server/uploads/.gitkeep โ keep the directory itself but not its contents
Files You SHOULD Commit
| File | Commit? | Reason |
|---|---|---|
| package.json | โ Always | Defines project dependencies and scripts |
| package-lock.json | โ Always | Ensures reproducible installs on all machines |
| .env.example | โ Always | Documents required variables without exposing secrets |
| .gitignore | โ Always | Must be in repo to protect all collaborators |
| node_modules/ | ๐ซ Never | Regenerated from package-lock.json by npm install |
| .env | ๐ซ Never | Contains real secrets โ JWT_SECRET, DB passwords |
| dist/ or build/ | ๐ซ Usually not | Generated at deploy time โ not source code |
Common Mistakes
Mistake 1 โ Accidentally committing node_modules
โ Wrong โ running git add . before creating .gitignore:
git init
git add . # adds ALL files including node_modules โ hundreds of thousands of files!
โ Correct โ always create .gitignore before the first git add. If node_modules is already tracked, remove it from Git history:
# Remove node_modules from tracking (keeps local copy)
echo "node_modules/" >> .gitignore
git rm -r --cached node_modules
git commit -m "remove node_modules from tracking"
Mistake 2 โ Deleting package-lock.json
โ Wrong โ deleting the lock file because it “causes conflicts”:
Without package-lock.json:
Developer A installs express โ gets 4.18.3
Developer B installs express โ gets 4.18.4 (released next day)
CI server installs express โ gets 4.19.0 (released next week)
โ Three different versions in production โ unpredictable behaviour
โ Correct โ commit package-lock.json and resolve merge conflicts in it properly. The lock file is your guarantee of consistent installs.
Mistake 3 โ Committing the .env file
โ Wrong โ .env containing real credentials committed to a public repo:
JWT_SECRET=supersecret123
MONGODB_URI=mongodb+srv://admin:realpassword@cluster.mongodb.net/prod
# Now public on GitHub โ database can be accessed by anyone, forever
# (Git history preserves it even if you delete the file in a later commit)
โ Correct โ add .env to .gitignore before creating it. Provide a .env.example with placeholder values instead. If you already committed it, rotate all exposed credentials immediately.
Quick Reference
| Task | Command |
|---|---|
| Install all dependencies | npm install |
| Reproducible install (CI) | npm ci |
| Clean reinstall | rm -rf node_modules && npm install |
| Remove from Git tracking | git rm -r --cached node_modules |
| Check what git will stage | git status |
| Verify .gitignore is working | git check-ignore -v node_modules |