0x55aa
← Back to Blog

package-lock.json: The File Nobody Reads But Everyone Needs šŸ”’

•11 min read

package-lock.json: The File Nobody Reads But Everyone Needs šŸ”’

Real confession: I once deleted package-lock.json because "it was causing merge conflicts and seemed useless." Pushed to production. Everything broke. Different team members had different dependency versions. Three hours of debugging later, I learned a VERY expensive lesson about npm's lockfile! 😱

When I was building Node.js APIs at Acodez, I thought package.json was all that mattered. The lockfile? Just generated noise, right? WRONG. That 10,000-line JSON file is literally the difference between "works on my machine" and "works everywhere"!

Coming from Laravel where composer.lock is treated as sacred, I should have known better. But npm's ecosystem is... special. Let me save you from the pain I went through!

What Even Is package-lock.json? šŸ¤”

package-lock.json = The EXACT version tree of every dependency in your project.

Think of it like a restaurant recipe:

  • package.json: "Add some flour, sugar, eggs" (vague ingredients)
  • package-lock.json: "Add EXACTLY 250g flour from Bob's Mill batch #4521, 200g sugar from C&H lot #8832" (precise inventory)

The magic: Everyone gets the EXACT same dependency versions, every time!

The trap: Ignoring it means "it works on my machine" becomes your team's motto! šŸ”„

The Production Disaster I Caused šŸ’„

My "brilliant" move at Acodez:

# Me, annoyed at merge conflicts:
git rm package-lock.json
git commit -m "Remove annoying lockfile"
git push

# Added to .gitignore (BIG MISTAKE!)
echo "package-lock.json" >> .gitignore

# Worked fine on my machine! āœ…
npm install
npm start
# "Everything works!"

What happened in production:

# CI/CD Pipeline (fresh install):
npm install  # Gets DIFFERENT versions than my local!

# Deploy to production...
# App starts...
# Random crashes! šŸ’€
# "TypeError: Cannot read property 'xyz' of undefined"
# "Module 'some-dependency' has no exported member 'Foo'"

# Team member tries to debug locally:
npm install  # Gets ANOTHER set of versions!
# "It works for me! Must be a production issue!"

# 3 hours of debugging later...
# Me: "Oh... I deleted the lockfile..."
# Team: "YOU WHAT?!"

Why it broke:

// package.json (what I committed):
{
  "dependencies": {
    "express": "^4.18.0",    // ^ means "compatible version"
    "lodash": "~4.17.0",     // ~ means "approximately this version"
    "axios": "^1.4.0"
  }
}

// What I got locally (had cached versions):
express@4.18.0
lodash@4.17.15
axios@1.4.0

// What CI/CD got (latest compatible versions):
express@4.18.2  // Patch update - breaking change snuck in!
lodash@4.17.21  // Minor update - different behavior
axios@1.6.5     // New version - API changed!

// Result: Different dependency trees = CHAOS!

Coming from Laravel: In Composer, composer.lock is treated as sacred. Delete it? Code review rejection! In npm world, developers delete lockfiles "to fix merge conflicts" and wonder why production breaks. Don't be that developer! šŸ˜…

Semantic Versioning: The Lie We Tell Ourselves šŸ“¦

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

^1.2.3 → Allows: 1.2.3, 1.2.4, 1.3.0, 1.999.999
         Blocks: 2.0.0

~1.2.3 → Allows: 1.2.3, 1.2.4, 1.2.999
         Blocks: 1.3.0

1.2.3  → Allows: ONLY 1.2.3 (exact version)

The theory: Patch and minor updates are "safe" and won't break your code!

The reality: LOL! 🤣

Real Semver Horror Story from Production:

# package.json
"some-popular-package": "^3.5.0"

# Developer A (installed Jan 1):
npm install
# Gets: [email protected]
# Everything works! āœ…

# Developer B (installed Jan 15):
npm install
# Gets: [email protected]  # "Minor" update
# Tests failing! Why?!

# What happened:
# v3.6.0: "Minor feature - changed default export format"
# v3.7.0: "Fixed bug - removed deprecated method you were using"
# Both are "minor" updates! Both broke things!

A pattern I learned the hard way:

// Old code (worked with v3.5.0):
const lib = require('some-package');
lib.doThing();  // Default export was a function

// New version (v3.6.0 - "minor" update):
const lib = require('some-package');
lib.doThing();  // TypeError: lib.doThing is not a function
// Now it's: lib.default.doThing()
// "Minor" update, major breakage!

The lockfile saves you:

// package-lock.json (committed to git):
{
  "packages": {
    "node_modules/some-package": {
      "version": "3.5.0",  // EXACT version locked!
      "resolved": "https://registry.npmjs.org/some-package/-/some-package-3.5.0.tgz",
      "integrity": "sha512-abc123..."  // Checksum! Prevents tampering!
    }
  }
}

// Everyone on your team gets EXACTLY 3.5.0
// No surprises! No "works on my machine"!

The Merge Conflict Temptation 🚨

The scenario every developer faces:

# You: Working on feature branch
npm install some-package
git add package-lock.json
git commit -m "Add some-package"

# Meanwhile, teammate: On main branch
npm install other-package
# Updates package-lock.json (different dependency tree!)

# You: Try to merge
git merge main
# CONFLICT in package-lock.json!
# 5000 lines of gibberish!

# Developer's first instinct:
git checkout --theirs package-lock.json  # WRONG!
git checkout --ours package-lock.json    # ALSO WRONG!
rm package-lock.json; npm install        # VERY WRONG!

The CORRECT solution:

# When package-lock.json conflicts:

# Step 1: Accept EITHER version (doesn't matter which)
git checkout --theirs package-lock.json

# Step 2: Regenerate lockfile with both changes
npm install

# Step 3: Commit the regenerated lockfile
git add package-lock.json
git commit -m "Merge package-lock.json"

# This merges BOTH dependency trees correctly!

Why this works: npm install reads package.json (which git merged correctly) and regenerates the lockfile with BOTH your dependencies and your teammate's!

Pro tip: I now use this in my .git/config:

# Add merge strategy for package-lock.json
git config merge.npm.driver "npm install --package-lock-only"

The "Just Run npm install" Trap 🪤

Another common scenario:

# New team member joins:
git clone repo
npm install

# Weeks later, bug appears:
"Hey, I can't reproduce this bug!"
"It works fine on my machine!"

# Debugging:
npm list some-dependency
# You: [email protected]
# Them: [email protected]

# WHY?! We have a lockfile!

# Answer: They ran this at some point:
npm install some-package  # Updates lockfile!
# But didn't commit the lockfile change!

Rules I follow at Acodez:

  1. ALWAYS commit lockfile changes (treat it like code!)
  2. NEVER run npm install <package> without committing (or you'll have phantom versions)
  3. Run npm ci in CI/CD (uses lockfile, fails if package.json differs)
  4. Add lockfile to code review (yes, even the huge diffs!)

npm install vs npm ci: The Difference That Matters šŸ”„

npm install:

  • Reads package.json AND package-lock.json
  • If they disagree, updates the lockfile
  • Installs dependencies
  • Use in development when adding/updating packages

npm ci (Continuous Integration):

  • ONLY reads package-lock.json
  • If package.json and lockfile disagree, FAILS!
  • Deletes node_modules and reinstalls from scratch
  • Use in CI/CD and production for exact reproducibility

Real example from my pipeline:

# .github/workflows/deploy.yml

# BAD (what I used to do):
- name: Install dependencies
  run: npm install  # Might update lockfile in CI!

# GOOD (what I do now):
- name: Install dependencies
  run: npm ci  # Fails if lockfile is out of sync!

Why npm ci is better for CI/CD:

# Scenario: Developer forgot to commit lockfile update

# With npm install (CI/CD):
npm install  # Silently updates lockfile in CI
npm test     # Tests pass
# Deploy to production
# Production has DIFFERENT versions than dev! šŸ’€

# With npm ci (CI/CD):
npm ci  # ERROR: package-lock.json out of sync with package.json
# Build fails!
# Developer forced to fix before merging!

The Dependency Hell I Didn't Know I Had šŸ”„

The sneaky problem with nested dependencies:

# You install one package:
npm install express

# What actually gets installed:
[email protected]
ā”œā”€ā”€ [email protected]
│   ā”œā”€ā”€ [email protected]
│   ā”œā”€ā”€ [email protected]
│   │   ā”œā”€ā”€ [email protected]
│   │   ā”œā”€ā”€ [email protected]
│   │   └── [email protected]
│   └── [email protected]
ā”œā”€ā”€ [email protected]
ā”œā”€ā”€ [email protected]
│   └── [email protected]
└── ... (50 more packages!)

# ONE package → 50+ dependencies!
# Each with their own version ranges!
# Each could update without you knowing!

The attack vector I discovered:

# Without lockfile:
npm install

# A "minor" update in a nested dependency:
[email protected]
└── [email protected]  # You installed when this was 4.5.0
    # New version added MALICIOUS CODE!
    # You never explicitly installed this!
    # You never reviewed it!
    # But your app now runs it!

# With lockfile:
# deep-nested-dep LOCKED at 4.5.0
# No surprise updates!
# Security!

Check your dependency tree:

# See what's actually installed:
npm list --depth=3

# Check for outdated packages:
npm outdated

# Check for security vulnerabilities:
npm audit

# Fix vulnerabilities (updates lockfile!):
npm audit fix

Common Mistakes I See (And Made) šŸ™ˆ

Mistake #1: Adding Lockfile to .gitignore

# NEVER DO THIS!
echo "package-lock.json" >> .gitignore

# Why it's bad:
# - Every developer gets different versions
# - Production gets different versions than dev
# - "Works on my machine" guaranteed!
# - Debugging nightmare!

Mistake #2: Manually Editing Lockfile

# NEVER manually edit package-lock.json!
# It has checksums (integrity field)
# Manual edits = corrupted lockfile
# npm will regenerate it anyway!

# Instead:
npm install <package>@<version>  # Let npm update it!

Mistake #3: Deleting node_modules Without Lockfile

# Common "debugging" attempt:
rm -rf node_modules
npm install  # Without lockfile = random versions!

# Better:
rm -rf node_modules
npm ci  # Uses lockfile for exact versions!

Mistake #4: Not Reviewing Lockfile Changes

# Developer adds innocuous package:
npm install lodash

# Lockfile diff: +5000 lines!
# Why? lodash has 0 dependencies!
# Answer: Other packages got updated too!

# ALWAYS review lockfile changes in PR:
git diff package-lock.json | grep "version"
# See what actually changed!

Security: The SHA-512 Checksum šŸ”

Every package in lockfile has an integrity hash:

{
  "node_modules/express": {
    "version": "4.18.2",
    "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
    "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ=="
  }
}

What this means:

  1. npm downloads package
  2. Computes SHA-512 hash of downloaded file
  3. Compares to lockfile hash
  4. If they don't match → ERROR! (package was tampered with!)

Real scenario this prevented:

# Attacker compromises npm registry
# Replaces [email protected] with malicious version
# Same version number, different code!

# Without lockfile:
npm install
# Gets malicious package! šŸ’€

# With lockfile:
npm install
# SHA-512 doesn't match!
# npm ERR! integrity checksum failed
# Installation blocked! āœ…

This is why you commit the lockfile: Not just for version locking, but for SECURITY!

The Lockfile Detective Work šŸ”

When something breaks after npm install:

# Check what changed:
git diff HEAD package-lock.json

# See full dependency tree:
npm list

# Check specific package version:
npm list express

# See why a package was installed:
npm why some-deep-dependency
# Output: express > body-parser > some-deep-dependency
# "Oh, it's a transitive dependency!"

# Check for duplicate versions (bloat!):
npm dedupe  # Simplifies dependency tree

Your Lockfile Checklist āœ…

Before you commit:

  • package-lock.json is committed to git
  • NOT in .gitignore
  • Lockfile changes reviewed in PR (even if huge)
  • CI/CD uses npm ci (not npm install)
  • No manual edits to lockfile
  • Dependencies match between dev/staging/prod
  • npm audit shows no critical vulnerabilities
  • Lockfile matches package.json (npm ci succeeds)

Pro Tips from Production šŸŽÆ

Tip #1: Audit Your Dependencies Regularly

# Check for vulnerabilities:
npm audit

# Auto-fix non-breaking updates:
npm audit fix

# See what will change before fixing:
npm audit fix --dry-run

# Update to latest (might break things!):
npm update

Tip #2: Use Exact Versions for Critical Packages

{
  "dependencies": {
    "express": "4.18.2",      // Exact version (no ^)
    "mongoose": "7.0.3",       // Exact version
    "lodash": "^4.17.21"       // Less critical = allow updates
  }
}

Tip #3: Lock Down Production Installs

# In Dockerfile or deployment script:
npm ci --only=production  # No devDependencies!
# Faster, smaller, reproducible!

Tip #4: Check Lockfile Version

// package-lock.json (top of file):
{
  "lockfileVersion": 2,  // npm v7+
  // vs
  "lockfileVersion": 1   // npm v5-6
}

// Version 2 is faster and more secure!
// Upgrade npm if you're on version 1!

The Bottom Line

package-lock.json isn't noise - it's your insurance policy against "works on my machine" syndrome!

The essentials:

  1. ALWAYS commit lockfile (treat it as critical as package.json)
  2. Use npm ci in CI/CD (exact reproducibility)
  3. Review lockfile changes (catch unexpected updates)
  4. Never delete it to fix merge conflicts (regenerate with npm install)
  5. Run npm audit regularly (security vulnerabilities)

When I was building Node.js APIs at Acodez, understanding lockfiles saved us from countless "but it works locally!" debugging sessions. Coming from Laravel where Composer enforces this, npm gives you freedom - but with freedom comes the responsibility to NOT delete your lockfile! šŸš€

Think of package-lock.json as your project's DNA sequence - exact, reproducible, and critical for survival. Delete it and you get random mutations (dependency chaos). Keep it and you get exact clones (reproducible builds)!


Got lockfile horror stories? Share them on LinkedIn - npm chaos makes the best war stories!

Want to see my Node.js projects? Check out my GitHub - all with properly committed lockfiles, I promise! šŸ˜‰

P.S. - If package-lock.json is in your .gitignore, go remove it RIGHT NOW. Your future self (and your team) will thank you! šŸ”’āœØ